class AnchorCompatibilityReporter(BaseCompatibilityReporter):

    objectName = "Anchor"

    def __init__(self, anchor1, anchor2):
        super(AnchorCompatibilityReporter, self).__init__(anchor1, anchor2)
        self.nameDifference = False

    anchor1 = dynamicProperty("object1")
    anchor1Name = dynamicProperty("object1Name")
    anchor2 = dynamicProperty("object2")
    anchor2Name = dynamicProperty("object2Name")

    def report(self, showOK=True, showWarnings=True):
        anchor1 = self.anchor1
        anchor2 = self.anchor2
        report = []
        if self.nameDifference:
            name1 = anchor1.name
            name2 = anchor2.name
            text = ("{anchor1Name} has name {name1} | "
                    "{anchor2Name} has name {name2}").format(
                        anchor1Name=self.anchor1Name,
                        name1=name1,
                        anchor2Name=self.anchor2Name,
                        name2=name2)
            report.append(self.formatWarningString(text))
        if report or showOK:
            report.insert(0, self.title)
        return "\n".join(report)
class GuidelineCompatibilityReporter(BaseCompatibilityReporter):

    objectName = "Guideline"

    def __init__(self, guideline1, guideline2):
        super(GuidelineCompatibilityReporter,
              self).__init__(guideline1, guideline2)
        self.nameDifference = False

    guideline1 = dynamicProperty("object1")
    guideline1Name = dynamicProperty("object1Name")
    guideline2 = dynamicProperty("object2")
    guideline2Name = dynamicProperty("object2Name")

    def report(self, showOK=True, showWarnings=True):
        guideline1 = self.guideline1
        guideline2 = self.guideline2
        report = []
        if self.nameDifference:
            name1 = guideline1.name
            name2 = guideline2.name
            text = ("{guideline1Name} has name {name1} | "
                    "{guideline2Name} has name {name2}").format(
                        guideline1Name=self.guideline1Name,
                        name1=name1,
                        guideline2Name=self.guideline2Name,
                        name2=name2)
            report.append(self.formatWarningString(text))
        if report or showOK:
            report.insert(0, self.title)
        return "\n".join(report)
class ComponentCompatibilityReporter(BaseCompatibilityReporter):

    objectName = "Component"

    def __init__(self, component1, component2):
        super(ComponentCompatibilityReporter,
              self).__init__(component1, component2)
        self.baseDifference = False

    component1 = dynamicProperty("object1")
    component1Name = dynamicProperty("object1Name")
    component2 = dynamicProperty("object2")
    component2Name = dynamicProperty("object2Name")

    def report(self, showOK=True, showWarnings=True):
        component1 = self.component1
        component2 = self.component2
        report = []
        if self.baseDifference:
            name1 = component1.baseName
            name2 = component2.baseName
            text = ("{component1Name} has base glyph {name1} | "
                    "{component2Name} has base glyph {name2}").format(
                        component1Name=self.component1Name,
                        name1=name1,
                        component2Name=self.component2Name,
                        name2=name2)
            report.append(self.formatWarningString(text))
        if report or showOK:
            report.insert(0, self.title)
        return "\n".join(report)
class SegmentCompatibilityReporter(BaseCompatibilityReporter):

    objectName = "Segment"

    def __init__(self, contour1, contour2):
        super(SegmentCompatibilityReporter, self).__init__(contour1, contour2)
        self.typeDifference = False

    segment1 = dynamicProperty("object1")
    segment1Name = dynamicProperty("object1Name")
    segment2 = dynamicProperty("object2")
    segment2Name = dynamicProperty("object2Name")

    def report(self, showOK=True, showWarnings=True):
        segment1 = self.segment1
        segment2 = self.segment2
        report = []
        if self.typeDifference:
            type1 = segment1.type
            type2 = segment2.type
            text = "{segment1Name} is {type1} | {segment2Name} is {type2}".format(
                segment1Name=self.segment1Name,
                type1=type1,
                segment2Name=self.segment2Name,
                type2=type2)
            report.append(self.formatFatalString(text))
        if report or showOK:
            report.insert(0, self.title)
        return "\n".join(report)
class ContourCompatibilityReporter(BaseCompatibilityReporter):

    objectName = "Contour"

    def __init__(self, contour1, contour2):
        super(ContourCompatibilityReporter, self).__init__(contour1, contour2)
        self.openDifference = False
        self.directionDifference = False
        self.segmentCountDifference = False
        self.segments = []

    contour1 = dynamicProperty("object1")
    contour1Name = dynamicProperty("object1Name")
    contour2 = dynamicProperty("object2")
    contour2Name = dynamicProperty("object2Name")

    def report(self, showOK=True, showWarnings=True):
        contour1 = self.contour1
        contour2 = self.contour2
        report = []
        if self.segmentCountDifference:
            text = self.reportCountDifference(subObjectName="segments",
                                              object1Name=self.contour1Name,
                                              object1Count=len(contour1),
                                              object2Name=self.contour2Name,
                                              object2Count=len(contour2))
            report.append(self.formatFatalString(text))
        if self.openDifference:
            state1 = state2 = "closed"
            if contour1.open:
                state1 = "open"
            if contour2.open:
                state2 = "open"
            text = "{contour1Name} is {state1} | {contour2Name} is {state2}".format(
                contour1Name=self.contour1Name,
                state1=state1,
                contour2Name=self.contour2Name,
                state2=state2)
            report.append(self.formatFatalString(text))
        if self.directionDifference:
            state1 = state2 = "counter-clockwise"
            if contour1.clockwise:
                state1 = "clockwise"
            if contour2.clockwise:
                state2 = "clockwise"
            text = "{contour1Name} is {state1} | {contour2Name} is {state2}".format(
                contour1Name=self.contour1Name,
                state1=state1,
                contour2Name=self.contour2Name,
                state2=state2)
            report.append(self.formatFatalString(text))
        report += self.reportSubObjects(self.segments,
                                        showOK=showOK,
                                        showWarnings=showWarnings)
        if report or showOK:
            report.insert(0, self.title)
        return "\n".join(report)
Esempio n. 6
0
class LayerCompatibilityReporter(BaseCompatibilityReporter):

    objectName = "Layer"

    def __init__(self, layer1, layer2):
        super(LayerCompatibilityReporter, self).__init__(layer1, layer2)
        self.glyphCountDifference = False
        self.glyphsMissingFromLayer2 = []
        self.glyphsMissingInLayer1 = []
        self.glyphs = []

    layer1 = dynamicProperty("object1")
    layer1Name = dynamicProperty("object1Name")
    layer2 = dynamicProperty("object2")
    layer2Name = dynamicProperty("object2Name")

    def report(self, showOK=True, showWarnings=True):
        layer1 = self.layer1
        layer2 = self.layer2
        report = []
        if self.glyphCountDifference:
            text = self.reportCountDifference(subObjectName="glyphs",
                                              object1Name=self.layer1Name,
                                              object1Count=len(layer1),
                                              object2Name=self.layer2Name,
                                              object2Count=len(layer2))
            report.append(self.formatWarningString(text))
        if len(self.glyphsMissingFromLayer2) != 0:
            for name in self.glyphsMissingFromLayer2:
                text = self.reportDifferences(
                    object1Name=self.layer1Name,
                    subObjectName="glyph",
                    subObjectID=name,
                    object2Name=self.layer2Name,
                )
                report.append(self.formatWarningString(text))
        if len(self.glyphsMissingInLayer1) != 0:
            for name in self.glyphsMissingInLayer1:
                text = self.reportDifferences(
                    object1Name=self.layer2Name,
                    subObjectName="glyph",
                    subObjectID=name,
                    object2Name=self.layer1Name,
                )
                report.append(self.formatWarningString(text))
        report += self.reportSubObjects(self.glyphs,
                                        showOK=showOK,
                                        showWarnings=showWarnings)

        if report or showOK:
            report.insert(0, self.title)
        return "\n".join(report)
Esempio n. 7
0
class BaseImage(BaseObject, TransformationMixin, PointPositionMixin, SelectionMixin):

    copyAttributes = (
        "transformation",
        "color",
        "data"
    )

    def _reprContents(self):
        contents = [
            "offset='({x}, {y})'".format(x=self.offset[0], y=self.offset[1]),
        ]
        if self.color:
            contents.append("color=%r" % str(self.color))
        if self.glyph is not None:
            contents.append("in glyph")
            contents += self.glyph._reprContents()
        return contents

    def __bool__(self):
        if len(self.data) == 0 or self.data is None:
            return False
        else:
            return True

    __nonzero__ = __bool__

    # -------
    # Parents
    # -------

    def getParent(self):
        """
        This is a backwards compatibility method.
        """
        return self.glyph

    # Glyph

    _glyph = None

    glyph = dynamicProperty("glyph", "The image's parent :class:`BaseGlyph`.")

    def _get_glyph(self):
        if self._glyph is None:
            return None
        return self._glyph()

    def _set_glyph(self, glyph):
        assert self._glyph is None
        if glyph is not None:
            glyph = reference(glyph)
        self._glyph = glyph

    # Layer

    layer = dynamicProperty("layer", "The image's parent :class:`BaseLayer`.")

    def _get_layer(self):
        if self._glyph is None:
            return None
        return self.glyph.layer

    # Font

    font = dynamicProperty("font", "The image's parent :class:`BaseFont`.")

    def _get_font(self):
        if self._glyph is None:
            return None
        return self.glyph.font

    # ----------
    # Attributes
    # ----------

    # Transformation

    transformation = dynamicProperty(
        "base_transformation",
        """
        The image's :ref:`type-transformation`.
        This defines the image's position, scale,
        and rotation. ::

            >>> image.transformation
            (1, 0, 0, 1, 0, 0)
            >>> image.transformation = (2, 0, 0, 2, 100, -50)
        """
    )

    def _get_base_transformation(self):
        value = self._get_transformation()
        value = normalizers.normalizeTransformationMatrix(value)
        return value

    def _set_base_transformation(self, value):
        value = normalizers.normalizeTransformationMatrix(value)
        self._set_transformation(value)

    def _get_transformation(self):
        """
        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_transformation(self, value):
        """
        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    offset = dynamicProperty(
        "base_offset",
        """
        The image's offset. This is a shortcut to the offset
        values in :attr:`transformation`. This must be an
        iterable containing two :ref:`type-int-float` values
        defining the x and y values to offset the image by. ::

            >>> image.offset
            (0, 0)
            >>> image.offset = (100, -50)
        """
    )

    def _get_base_offset(self):
        value = self._get_offset()
        value = normalizers.normalizeTransformationOffset(value)
        return value

    def _set_base_offset(self, value):
        value = normalizers.normalizeTransformationOffset(value)
        self._set_offset(value)

    def _get_offset(self):
        """
        Subclasses may override this method.
        """
        sx, sxy, syx, sy, ox, oy = self.transformation
        return (ox, oy)

    def _set_offset(self, value):
        """
        Subclasses may override this method.
        """
        sx, sxy, syx, sy, ox, oy = self.transformation
        ox, oy = value
        self.transformation = (sx, sxy, syx, sy, ox, oy)

    scale = dynamicProperty(
        "base_scale",
        """
        The image's scale. This is a shortcut to the scale
        values in :attr:`transformation`. This must be an
        iterable containing two :ref:`type-int-float` values
        defining the x and y values to scale the image by. ::

            >>> image.scale
            (1, 1)
            >>> image.scale = (2, 2)
        """
    )

    def _get_base_scale(self):
        value = self._get_scale()
        value = normalizers.normalizeTransformationScale(value)
        return value

    def _set_base_scale(self, value):
        value = normalizers.normalizeTransformationScale(value)
        self._set_scale(value)

    def _get_scale(self):
        """
        Subclasses may override this method.
        """
        sx, sxy, syx, sy, ox, oy = self.transformation
        return (sx, sy)

    def _set_scale(self, value):
        """
        Subclasses may override this method.
        """
        sx, sxy, syx, sy, ox, oy = self.transformation
        sx, sy = value
        self.transformation = (sx, sxy, syx, sy, ox, oy)

    # Color

    color = dynamicProperty(
        "base_color",
        """
        The image's color. This will be a
        :ref:`type-color` or ``None``. ::

            >>> image.color
            None
            >>> image.color = (1, 0, 0, 0.5)
        """
    )

    def _get_base_color(self):
        value = self._get_color()
        if value is not None:
            value = normalizers.normalizeColor(value)
            value = Color(value)
        return value

    def _set_base_color(self, value):
        if value is not None:
            value = normalizers.normalizeColor(value)
        self._set_color(value)

    def _get_color(self):
        """
        Return the color value as a color tuple or None.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_color(self, value):
        """
        value will be a color tuple or None.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # Data

    data = dynamicProperty(
        "data",
        """
        The image's raw byte data. The possible
        formats are defined by each environment.
        """
    )

    def _get_base_data(self):
        return self._get_data()

    def _set_base_data(self, value):
        self._set_data(value)

    def _get_data(self):
        """
        This must return raw byte data.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_data(self, value):
        """
        value will be raw byte data.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # --------------
    # Transformation
    # --------------

    def _transformBy(self, matrix, **kwargs):
        """
        Subclasses may override this method.
        """
        t = transform.Transform(*matrix)
        transformation = t.transform(self.transformation)
        self.transformation = tuple(transformation)

    # -------------
    # Normalization
    # -------------

    def round(self):
        """
        Round offset coordinates.
        """
        self._round()

    def _round(self):
        """
        Subclasses may override this method.
        """
        x, y = self.offset
        x = normalizers.normalizeRounding(x)
        y = normalizers.normalizeRounding(y)
        self.offset = (x, y)
Esempio n. 8
0
class BasePoint(BaseObject, TransformationMixin, PointPositionMixin,
                SelectionMixin, IdentifierMixin, DeprecatedPoint,
                RemovedPoint):
    """
    A point object. This object is almost always
    created with :meth:`BaseContour.appendPoint`,
    the pen returned by :meth:`BaseGlyph.getPen`
    or the point pen returned by :meth:`BaseGLyph.getPointPen`.
    An orphan point can be created like this::

        >>> point = RPoint()
    """

    copyAttributes = ("type", "smooth", "x", "y", "name")

    def _reprContents(self):
        contents = [
            "%s" % self.type,
            ("({x}, {y})".format(x=self.x, y=self.y)),
        ]
        if self.name is not None:
            contents.append("name='%s'" % self.name)
        if self.smooth:
            contents.append("smooth=%r" % self.smooth)
        return contents

    # -------
    # Parents
    # -------

    # Contour

    _contour = None

    contour = dynamicProperty("contour",
                              "The point's parent :class:`BaseContour`.")

    def _get_contour(self):
        if self._contour is None:
            return None
        return self._contour()

    def _set_contour(self, contour):
        assert self._contour is None
        if contour is not None:
            contour = reference(contour)
        self._contour = contour

    # Glyph

    glyph = dynamicProperty("glyph", "The point's parent :class:`BaseGlyph`.")

    def _get_glyph(self):
        if self._contour is None:
            return None
        return self.contour.glyph

    # Layer

    layer = dynamicProperty("layer", "The point's parent :class:`BaseLayer`.")

    def _get_layer(self):
        if self._contour is None:
            return None
        return self.glyph.layer

    # Font

    font = dynamicProperty("font", "The point's parent :class:`BaseFont`.")

    def _get_font(self):
        if self._contour is None:
            return None
        return self.glyph.font

    # ----------
    # Attributes
    # ----------

    # type

    type = dynamicProperty(
        "base_type", """
        The point type defined with a :ref:`type-string`.
        The possible types are:

        +----------+---------------------------------+
        | move     | An on-curve move to.            |
        +----------+---------------------------------+
        | line     | An on-curve line to.            |
        +----------+---------------------------------+
        | curve    | An on-curve cubic curve to.     |
        +----------+---------------------------------+
        | qcurve   | An on-curve quadratic curve to. |
        +----------+---------------------------------+
        | offcurve | An off-curve.                   |
        +----------+---------------------------------+
        """)

    def _get_base_type(self):
        value = self._get_type()
        value = normalizers.normalizePointType(value)
        return value

    def _set_base_type(self, value):
        value = normalizers.normalizePointType(value)
        self._set_type(value)

    def _get_type(self):
        """
        This is the environment implementation
        of :attr:`BasePoint.type`. This must
        return a :ref:`type-string` defining
        the point type.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_type(self, value):
        """
        This is the environment implementation
        of :attr:`BasePoint.type`. **value**
        will be a :ref:`type-string` defining
        the point type. It will have been normalized
        with :func:`normalizers.normalizePointType`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # smooth

    smooth = dynamicProperty(
        "base_smooth", """
        A ``bool`` indicating if the point is smooth or not. ::

            >>> point.smooth
            False
            >>> point.smooth = True

        """)

    def _get_base_smooth(self):
        value = self._get_smooth()
        value = normalizers.normalizeBoolean(value)
        return value

    def _set_base_smooth(self, value):
        value = normalizers.normalizeBoolean(value)
        self._set_smooth(value)

    def _get_smooth(self):
        """
        This is the environment implementation of
        :attr:`BasePoint.smooth`. This must return
        a ``bool`` indicating the smooth state.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_smooth(self, value):
        """
        This is the environment implementation of
        :attr:`BasePoint.smooth`. **value** will
        be a ``bool`` indicating the smooth state.
        It will have been normalized with
        :func:`normalizers.normalizeBoolean`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # x

    x = dynamicProperty(
        "base_x", """
        The x coordinate of the point.
        It must be an :ref:`type-int-float`. ::

            >>> point.x
            100
            >>> point.x = 101
        """)

    def _get_base_x(self):
        value = self._get_x()
        value = normalizers.normalizeX(value)
        return value

    def _set_base_x(self, value):
        value = normalizers.normalizeX(value)
        self._set_x(value)

    def _get_x(self):
        """
        This is the environment implementation of
        :attr:`BasePoint.x`. This must return an
        :ref:`type-int-float`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_x(self, value):
        """
        This is the environment implementation of
        :attr:`BasePoint.x`. **value** will be
        an :ref:`type-int-float`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # y

    y = dynamicProperty(
        "base_y", """
        The y coordinate of the point.
        It must be an :ref:`type-int-float`. ::

            >>> point.y
            100
            >>> point.y = 101
        """)

    def _get_base_y(self):
        value = self._get_y()
        value = normalizers.normalizeY(value)
        return value

    def _set_base_y(self, value):
        value = normalizers.normalizeY(value)
        self._set_y(value)

    def _get_y(self):
        """
        This is the environment implementation of
        :attr:`BasePoint.y`. This must return an
        :ref:`type-int-float`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_y(self, value):
        """
        This is the environment implementation of
        :attr:`BasePoint.y`. **value** will be
        an :ref:`type-int-float`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # --------------
    # Identification
    # --------------

    # index

    index = dynamicProperty(
        "base_index", """
        The index of the point within the ordered
        list of the parent glyph's point. This
        attribute is read only. ::

            >>> point.index
            0
        """)

    def _get_base_index(self):
        value = self._get_index()
        value = normalizers.normalizeIndex(value)
        return value

    def _get_index(self):
        """
        Get the point's index.
        This must return an ``int``.

        Subclasses may override this method.
        """
        contour = self.contour
        if contour is None:
            return None
        return contour.points.index(self)

    # name

    name = dynamicProperty(
        "base_name", """
        The name of the point. This will be a
        :ref:`type-string` or ``None``.

            >>> point.name
            'my point'
            >>> point.name = None
        """)

    def _get_base_name(self):
        value = self._get_name()
        if value is not None:
            value = normalizers.normalizePointName(value)
        return value

    def _set_base_name(self, value):
        if value is not None:
            value = normalizers.normalizePointName(value)
        self._set_name(value)

    def _get_name(self):
        """
        This is the environment implementation of
        :attr:`BasePoint.name`. This must return a
        :ref:`type-string` or ``None``. The returned
        value will be normalized with
        :func:`normalizers.normalizePointName`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_name(self, value):
        """
        This is the environment implementation of
        :attr:`BasePoint.name`. **value** will be
        a :ref:`type-string` or ``None``. It will
        have been normalized with
        :func:`normalizers.normalizePointName`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # --------------
    # Transformation
    # --------------

    def _transformBy(self, matrix, **kwargs):
        """
        This is the environment implementation of
        :meth:`BasePoint.transformBy`.

        **matrix** will be a :ref:`type-transformation`.
        that has been normalized with
        :func:`normalizers.normalizeTransformationMatrix`.

        Subclasses may override this method.
        """
        t = transform.Transform(*matrix)
        x, y = t.transformPoint((self.x, self.y))
        self.x = x
        self.y = y

    # -------------
    # Normalization
    # -------------

    def round(self):
        """
        Round the point's coordinate.

            >>> point.round()

        This applies to the following:

        * x
        * y
        """
        self._round()

    def _round(self, **kwargs):
        """
        This is the environment implementation of
        :meth:`BasePoint.round`.

        Subclasses may override this method.
        """
        self.x = normalizers.normalizeRounding(self.x)
        self.y = normalizers.normalizeRounding(self.y)
Esempio n. 9
0
class BaseInfo(BaseObject, DeprecatedInfo, RemovedInfo):
    from ufoLib import fontInfoAttributesVersion3

    copyAttributes = set(fontInfoAttributesVersion3)
    copyAttributes.remove("guidelines")
    copyAttributes = tuple(copyAttributes)

    def _reprContents(self):
        contents = []
        if self.font is not None:
            contents.append("for font")
            contents += self.font._reprContents()
        return contents

    # -------
    # Parents
    # -------

    # Font

    _font = None

    font = dynamicProperty("font", "The info's parent font.")

    def _get_font(self):
        if self._font is None:
            return None
        return self._font()

    def _set_font(self, font):
        assert self._font is None or self._font() == font
        if font is not None:
            font = reference(font)
        self._font = font

    # ----------
    # Validation
    # ----------

    def _validateFontInfoAttributeValue(self, attr, value):
        from ufoLib import validateFontInfoVersion3ValueForAttribute
        valid = validateFontInfoVersion3ValueForAttribute(attr, value)
        if not valid:
            raise ValueError("Invalid value %s for attribute '%s'." %
                             (value, attr))
        return value

    # ----------
    # Attributes
    # ----------

    # has

    def __hasattr__(self, attr):
        from ufoLib import fontInfoAttributesVersion3
        if attr in fontInfoAttributesVersion3:
            return True
        return super(BaseInfo, self).__hasattr__(attr)

    # get

    def __getattribute__(self, attr):
        from ufoLib import fontInfoAttributesVersion3
        if attr != "guidelines" and attr in fontInfoAttributesVersion3:
            value = self._getAttr(attr)
            if value is not None:
                value = self._validateFontInfoAttributeValue(attr, value)
            return value
        return super(BaseInfo, self).__getattribute__(attr)

    def _getAttr(self, attr):
        """
        Subclasses may override this method.

        If a subclass does not override this method,
        it must implement '_get_attributeName' methods
        for all Info methods.
        """
        meth = "_get_%s" % attr
        if not hasattr(self, meth):
            raise AttributeError("No getter for attribute '%s'." % attr)
        meth = getattr(self, meth)
        value = meth()
        return value

    # set

    def __setattr__(self, attr, value):
        from ufoLib import fontInfoAttributesVersion3
        if attr != "guidelines" and attr in fontInfoAttributesVersion3:
            if value is not None:
                value = self._validateFontInfoAttributeValue(attr, value)
            return self._setAttr(attr, value)
        return super(BaseInfo, self).__setattr__(attr, value)

    def _setAttr(self, attr, value):
        """
        Subclasses may override this method.

        If a subclass does not override this method,
        it must implement '_set_attributeName' methods
        for all Info methods.
        """
        meth = "_set_%s" % attr
        if not hasattr(self, meth):
            raise AttributeError("No setter for attribute '%s'." % attr)
        meth = getattr(self, meth)
        meth(value)

    # -------------
    # Normalization
    # -------------

    def round(self):
        """
        Round the following attributes to integers:

        - unitsPerEm
        - descender
        - xHeight
        - capHeight
        - ascender
        - openTypeHeadLowestRecPPEM
        - openTypeHheaAscender
        - openTypeHheaDescender
        - openTypeHheaLineGap
        - openTypeHheaCaretSlopeRise
        - openTypeHheaCaretSlopeRun
        - openTypeHheaCaretOffset
        - openTypeOS2WidthClass
        - openTypeOS2WeightClass
        - openTypeOS2TypoAscender
        - openTypeOS2TypoDescender
        - openTypeOS2TypoLineGap
        - openTypeOS2WinAscent
        - openTypeOS2WinDescent
        - openTypeOS2SubscriptXSize
        - openTypeOS2SubscriptYSize
        - openTypeOS2SubscriptXOffset
        - openTypeOS2SubscriptYOffset
        - openTypeOS2SuperscriptXSize
        - openTypeOS2SuperscriptYSize
        - openTypeOS2SuperscriptXOffset
        - openTypeOS2SuperscriptYOffset
        - openTypeOS2StrikeoutSize
        - openTypeOS2StrikeoutPosition
        - openTypeVheaVertTypoAscender
        - openTypeVheaVertTypoDescender
        - openTypeVheaVertTypoLineGap
        - openTypeVheaCaretSlopeRise
        - openTypeVheaCaretSlopeRun
        - openTypeVheaCaretOffset
        - postscriptSlantAngle
        - postscriptUnderlineThickness
        - postscriptUnderlinePosition
        - postscriptBlueValues
        - postscriptOtherBlues
        - postscriptFamilyBlues
        - postscriptFamilyOtherBlues
        - postscriptStemSnapH
        - postscriptStemSnapV
        - postscriptBlueFuzz
        - postscriptBlueShift
        - postscriptDefaultWidthX
        - postscriptNominalWidthX
        """
        self._round()

    def _round(self, **kwargs):
        """
        Subclasses may override this method.
        """
        mathInfo = self._toMathInfo(guidelines=False)
        mathInfo = mathInfo.round()
        self._fromMathInfo(mathInfo, guidelines=False)

    # -------------
    # Interpolation
    # -------------

    def _toMathInfo(self, guidelines=True):
        """
        Subclasses may override this method.
        """
        import fontMath
        # A little trickery is needed here because MathInfo
        # handles font level guidelines. Those are not in this
        # object so we temporarily fake them just enough for
        # MathInfo and then move them back to the proper place.
        self.guidelines = []
        if guidelines:
            for guideline in self.font.guidelines:
                d = dict(x=guideline.x,
                         y=guideline.y,
                         angle=guideline.angle,
                         name=guideline.name,
                         identifier=guideline.identifier,
                         color=guideline.color)
                self.guidelines.append(d)
        info = fontMath.MathInfo(self)
        del self.guidelines
        return info

    def _fromMathInfo(self, mathInfo, guidelines=True):
        """
        Subclasses may override this method.
        """
        self.guidelines = []
        mathInfo.extractInfo(self)
        font = self.font
        if guidelines:
            for guideline in self.guidelines:
                font.appendGuideline(position=(guideline["x"], guideline["y"]),
                                     angle=guideline["angle"],
                                     name=guideline["name"],
                                     color=guideline["color"]
                                     # XXX identifier is lost
                                     )
        del self.guidelines

    def interpolate(self,
                    factor,
                    minInfo,
                    maxInfo,
                    round=True,
                    suppressError=True):
        """
        Interpolate all pairs between minInfo and maxInfo.
        The interpolation occurs on a 0 to 1.0 range where minInfo
        is located at 0 and maxInfo is located at 1.0.

        factor is the interpolation value. It may be less than 0
        and greater than 1.0. It may be a number (integer, float)
        or a tuple of two numbers. If it is a tuple, the first
        number indicates the x factor and the second number
        indicates the y factor.

        round indicates if the result should be rounded to integers.

        suppressError indicates if incompatible data should be ignored
        or if an error should be raised when such incompatibilities are found.
        """
        factor = normalizers.normalizeInterpolationFactor(factor)
        if not isinstance(minInfo, BaseInfo):
            raise TypeError(
                ("Interpolation to an instance of %r can not be "
                 "performed from an instance of %r.") %
                (self.__class__.__name__, minInfo.__class__.__name__))
        if not isinstance(maxInfo, BaseInfo):
            raise TypeError(
                ("Interpolation to an instance of %r can not be "
                 "performed from an instance of %r.") %
                (self.__class__.__name__, maxInfo.__class__.__name__))
        round = normalizers.normalizeBoolean(round)
        suppressError = normalizers.normalizeBoolean(suppressError)
        self._interpolate(factor,
                          minInfo,
                          maxInfo,
                          round=round,
                          suppressError=suppressError)

    def _interpolate(self,
                     factor,
                     minInfo,
                     maxInfo,
                     round=True,
                     suppressError=True):
        """
        Subclasses may override this method.
        """
        minInfo = minInfo._toMathInfo()
        maxInfo = maxInfo._toMathInfo()
        result = interpolate(minInfo, maxInfo, factor)
        if round:
            result = result.round()
        self._fromMathInfo(result)
class BaseCompatibilityReporter(object):

    objectName = "Base"

    def __init__(self, obj1, obj2):
        self._object1 = obj1
        self._object2 = obj2

    # status

    fatal = False
    warning = False

    def _get_title(self):
        title = "{object1Name} + {object2Name}".format(
            object1Name=self.object1Name, object2Name=self.object2Name)
        if self.fatal:
            return self.formatFatalString(title)
        elif self.warning:
            return self.formatWarningString(title)
        else:
            return self.formatOKString(title)

    title = dynamicProperty("title")

    # objects

    object1 = dynamicProperty("object1")
    object1Name = dynamicProperty("object1Name")

    def _get_object1(self):
        return self._object1

    def _get_object1Name(self):
        return self._getObjectName(self._object1)

    object2 = dynamicProperty("object2")
    object2Name = dynamicProperty("object2Name")

    def _get_object2(self):
        return self._object2

    def _get_object2Name(self):
        return self._getObjectName(self._object2)

    @staticmethod
    def _getObjectName(obj):
        if hasattr(obj, "name") and obj.name is not None:
            return "\"%s\"" % obj.name
        elif hasattr(obj, "identifier") and obj.identifier is not None:
            return "\"%s\"" % obj.identifier
        elif hasattr(obj, "index"):
            return "[%s]" % obj.index
        else:
            return "<%s>" % id(obj)

    # Report

    def __repr__(self):
        return self.report()

    def report(self, showOK=False, showWarnings=False):
        raise NotImplementedError

    def formatFatalString(self, text):
        return "[Fatal] {objectName}: ".format(
            objectName=self.objectName) + text

    def formatWarningString(self, text):
        return "[Warning] {objectName}: ".format(
            objectName=self.objectName) + text

    def formatOKString(self, text):
        return "[OK] {objectName}: ".format(objectName=self.objectName) + text

    @staticmethod
    def reportSubObjects(reporters, showOK=True, showWarnings=True):
        report = []
        for reporter in reporters:
            if showOK or reporter.fatal or (showWarnings and reporter.warning):
                report.append(repr(reporter))
        return report

    @staticmethod
    def reportCountDifference(subObjectName, object1Name, object1Count,
                              object2Name, object2Count):
        text = (
            "{object1Name} contains {object1Count} {subObjectName} | "
            "{object2Name} contains {object2Count} {subObjectName}").format(
                subObjectName=subObjectName,
                object1Name=object1Name,
                object1Count=object1Count,
                object2Name=object2Name,
                object2Count=object2Count)
        return text

    @staticmethod
    def reportOrderDifference(subObjectName, object1Name, object1Order,
                              object2Name, object2Order):
        text = ("{object1Name} has {subObjectName} ordered {object1Order} | "
                "{object2Name} has {object2Order}").format(
                    subObjectName=subObjectName,
                    object1Name=object1Name,
                    object1Order=object1Order,
                    object2Name=object2Name,
                    object2Order=object2Order)
        return text

    @staticmethod
    def reportDifferences(object1Name, subObjectName, subObjectID,
                          object2Name):
        text = ("{object1Name} contains {subObjectName} {subObjectID} "
                "not in {object2Name}").format(
                    object1Name=object1Name,
                    subObjectName=subObjectName,
                    subObjectID=subObjectID,
                    object2Name=object2Name,
                )
        return text
Esempio n. 11
0
class BaseLib(BaseDict, DeprecatedLib, RemovedLib):
    """
    A Lib object. This object normally created as part of a
    :class:`BaseFont`. An orphan Lib object can be created like this::

        >>> lib = RLib()

    This object behaves like a Python dictionary. Most of the dictionary
    functionality comes from :class:`BaseDict`, look at that object for the
    required environment implementation details.

    Lib uses :func:`normalizers.normalizeLibKey` to normalize the key of
    the ``dict``, and :func:`normalizers.normalizeLibValue` to normalize the
    value of the ``dict``.
    """

    keyNormalizer = normalizers.normalizeLibKey
    valueNormalizer = normalizers.normalizeLibValue

    def _reprContents(self):
        contents = []
        if self.glyph is not None:
            contents.append("in glyph")
            contents += self.glyph._reprContents()
        if self.font:
            contents.append("in font")
            contents += self.font._reprContents()
        return contents

    # -------
    # Parents
    # -------

    # Glyph

    _glyph = None

    glyph = dynamicProperty("glyph", "The lib's parent glyph.")

    def _get_glyph(self):
        if self._glyph is None:
            return None
        return self._glyph()

    def _set_glyph(self, glyph):
        assert self._font is None
        assert self._glyph is None or self._glyph() == glyph
        if glyph is not None:
            glyph = reference(glyph)
        self._glyph = glyph

    # Font

    _font = None

    font = dynamicProperty("font", "The lib's parent font.")

    def _get_font(self):
        if self._font is not None:
            return self._font()
        elif self._glyph is not None:
            return self.glyph.font
        return None

    def _set_font(self, font):
        assert self._font is None or self._font() == font
        assert self._glyph is None
        if font is not None:
            font = reference(font)
        self._font = font

    # Layer

    layer = dynamicProperty("layer", "The lib's parent layer.")

    def _get_layer(self):
        if self._glyph is None:
            return None
        return self.glyph.layer

    # ---------------------
    # RoboFab Compatibility
    # ---------------------

    def remove(self, key):
        """
        Removes a key from the Lib. **key** will be
        a :ref:`type-string` that is the key to
        be removed.

        This is a backwards compatibility method.
        """
        del self[key]

    def asDict(self):
        """
        Return the Lib as a ``dict``.

        This is a backwards compatibility method.
        """
        d = {}
        for k, v in self.items():
            d[k] = v
        return d

    # -------------------
    # Inherited Functions
    # -------------------

    def __contains__(self, key):
        """
        Tests to see if a lib name is in the Lib.
        **key** will be a :ref:`type-string`.
        This returns a ``bool`` indicating if the **key**
        is in the Lib. ::

            >>> "public.glyphOrder" in font.lib
            True
        """
        return super(BaseLib, self).__contains__(key)

    def __delitem__(self, key):
        """
        Removes **key** from the Lib. **key** is a :ref:`type-string`.::

            >>> del font.lib["public.glyphOrder"]
        """
        super(BaseLib, self).__delitem__(key)

    def __getitem__(self, key):
        """
        Returns the contents of the named lib. **key** is a
        :ref:`type-string`.
        The returned value will be a ``list`` of the lib contents.::

            >>> font.lib["public.glyphOrder"]
            ["A", "B", "C"]

        It is important to understand that any changes to the returned lib
        contents will not be reflected in the Lib object. If one wants to
        make a change to the lib contents, one should do the following::

            >>> lib = font.lib["public.glyphOrder"]
            >>> lib.remove("A")
            >>> font.lib["public.glyphOrder"] = lib
        """
        return super(BaseLib, self).__getitem__(key)

    def __iter__(self):
        """
        Iterates through the Lib, giving the key for each iteration. The
        order that the Lib will iterate though is not fixed nor is it
        ordered.::

            >>> for key in font.lib:
            >>>     print key
            "public.glyphOrder"
            "org.robofab.scripts.SomeData"
            "public.postscriptNames"
        """
        return super(BaseLib, self).__iter__()

    def __len__(self):
        """
        Returns the number of keys in Lib as an ``int``.::

            >>> len(font.lib)
            5
        """
        return super(BaseLib, self).__len__()

    def __setitem__(self, key, items):
        """
        Sets the **key** to the list of **items**. **key**
        is the lib name as a :ref:`type-string` and **items** is a
        ``list`` of items as :ref:`type-string`.

            >>> font.lib["public.glyphOrder"] = ["A", "B", "C"]
        """
        super(BaseLib, self).__setitem__(key, items)

    def clear(self):
        """
        Removes all keys from Lib,
        resetting the Lib to an empty dictionary. ::

            >>> font.lib.clear()
        """
        super(BaseLib, self).clear()

    def get(self, key, default=None):
        """
        Returns the contents of the named key.
        **key** is a :ref:`type-string`, and the returned values will
        either be ``list`` of key contents or ``None`` if no key was
        found. ::

            >>> font.lib["public.glyphOrder"]
            ["A", "B", "C"]

        It is important to understand that any changes to the returned key
        contents will not be reflected in the Lib object. If one wants to
        make a change to the key contents, one should do the following::

            >>> lib = font.lib["public.glyphOrder"]
            >>> lib.remove("A")
            >>> font.lib["public.glyphOrder"] = lib
        """
        return super(BaseLib, self).get(key, default)

    def items(self):
        """
        Returns a list of ``tuple`` of each key name and key items.
        Keys are :ref:`type-string` and key members are a ``list``
        of :ref:`type-string`. The initial list will be unordered.

            >>> font.lib.items()
            [("public.glyphOrder", ["A", "B", "C"]),
             ("public.postscriptNames", {'be': 'uni0431', 'ze': 'uni0437'})]
        """
        return super(BaseLib, self).items()

    def keys(self):
        """
        Returns a ``list`` of all the key names in Lib. This list will be
        unordered.::

            >>> font.lib.keys()
            ["public.glyphOrder", "org.robofab.scripts.SomeData",
             "public.postscriptNames"]
        """
        return super(BaseLib, self).keys()

    def pop(self, key, default=None):
        """
        Removes the **key** from the Lib and returns the ``list`` of
        key members. If no key is found, **default** is returned.
        **key** is a :ref:`type-string`. This must return either
        **default** or a ``list`` of items as :ref:`type-string`.

            >>> font.lib.pop("public.glyphOrder")
            ["A", "B", "C"]
        """
        return super(BaseLib, self).pop(key, default)

    def update(self, otherLib):
        """
        Updates the Lib based on **otherLib**. *otherLib** is a
        ``dict`` of keys. If a key from **otherLib** is in Lib
        the key members will be replaced by the key members from
        **otherLib**. If a key from **otherLib** is not in the Lib,
        it is added to the Lib. If Lib contain a key name that is not
        in *otherLib**, it is not changed.

            >>> font.lib.update(newLib)
        """
        super(BaseLib, self).update(otherLib)

    def values(self):
        """
        Returns a ``list`` of each named key's members. This will be a list
        of lists, the key members will be a ``list`` of :ref:`type-string`.
        The initial list will be unordered.

            >>> font.lib.items()
            [["A", "B", "C"], {'be': 'uni0431', 'ze': 'uni0437'}]
        """
        return super(BaseLib, self).values()
Esempio n. 12
0
class BaseBPoint(BaseObject, TransformationMixin, SelectionMixin,
                 DeprecatedBPoint, IdentifierMixin, RemovedBPoint):
    def _reprContents(self):
        contents = [
            "%s" % self.type,
            "anchor='({x}, {y})'".format(x=self.anchor[0], y=self.anchor[1]),
        ]
        return contents

    def _setPoint(self, point):
        if hasattr(self, "_point"):
            raise AssertionError("point for bPoint already set")
        self._point = point

    def __eq__(self, other):
        if hasattr(other, "_point"):
            return self._point == other._point
        return NotImplemented

    # this class should not be used in hashable
    # collections since it is dynamically generated.

    __hash__ = None

    # -------
    # Parents
    # -------

    # identifier

    def _get_identifier(self):
        """
        Subclasses may override this method.
        """
        return self._point.identifier

    def _getIdentifier(self):
        """
        Subclasses may override this method.
        """
        return self._point.getIdentifier()

    # Segment

    _segment = dynamicProperty("base_segment")

    def _get_base_segment(self):
        point = self._point
        for segment in self.contour.segments:
            if segment.onCurve == point:
                return segment

    _nextSegment = dynamicProperty("base_nextSegment")

    def _get_base_nextSegment(self):
        contour = self.contour
        if contour is None:
            return None
        segments = contour.segments
        segment = self._segment
        i = segments.index(segment) + 1
        if i >= len(segments):
            i = i % len(segments)
        nextSegment = segments[i]
        return nextSegment

    # Contour

    _contour = None

    contour = dynamicProperty("contour", "The bPoint's parent contour.")

    def _get_contour(self):
        if self._contour is None:
            return None
        return self._contour()

    def _set_contour(self, contour):
        if self._contour is not None:
            raise AssertionError("contour for bPoint already set")
        if contour is not None:
            contour = reference(contour)
        self._contour = contour

    # Glyph

    glyph = dynamicProperty("glyph", "The bPoint's parent glyph.")

    def _get_glyph(self):
        if self._contour is None:
            return None
        return self.contour.glyph

    # Layer

    layer = dynamicProperty("layer", "The bPoint's parent layer.")

    def _get_layer(self):
        if self._contour is None:
            return None
        return self.glyph.layer

    # Font

    font = dynamicProperty("font", "The bPoint's parent font.")

    def _get_font(self):
        if self._contour is None:
            return None
        return self.glyph.font

    # ----------
    # Attributes
    # ----------

    # anchor

    anchor = dynamicProperty("base_anchor", "The anchor point.")

    def _get_base_anchor(self):
        value = self._get_anchor()
        value = normalizers.normalizeCoordinateTuple(value)
        return value

    def _set_base_anchor(self, value):
        value = normalizers.normalizeCoordinateTuple(value)
        self._set_anchor(value)

    def _get_anchor(self):
        """
        Subclasses may override this method.
        """
        point = self._point
        return (point.x, point.y)

    def _set_anchor(self, value):
        """
        Subclasses may override this method.
        """
        pX, pY = self.anchor
        x, y = value
        dX = x - pX
        dY = y - pY
        self.moveBy((dX, dY))

    # bcp in

    bcpIn = dynamicProperty("base_bcpIn", "The incoming off curve.")

    def _get_base_bcpIn(self):
        value = self._get_bcpIn()
        value = normalizers.normalizeCoordinateTuple(value)
        return value

    def _set_base_bcpIn(self, value):
        value = normalizers.normalizeCoordinateTuple(value)
        self._set_bcpIn(value)

    def _get_bcpIn(self):
        """
        Subclasses may override this method.
        """
        segment = self._segment
        offCurves = segment.offCurve
        if offCurves:
            bcp = offCurves[-1]
            x, y = relativeBCPIn(self.anchor, (bcp.x, bcp.y))
        else:
            x = y = 0
        return (x, y)

    def _set_bcpIn(self, value):
        """
        Subclasses may override this method.
        """
        x, y = absoluteBCPIn(self.anchor, value)
        segment = self._segment
        if segment.type == "move" and value != (0, 0):
            raise FontPartsError(("Cannot set the bcpIn for the first "
                                  "point in an open contour."))
        else:
            offCurves = segment.offCurve
            if offCurves:
                # if the two off curves are located at the anchor
                # coordinates we can switch to a line segment type.
                if value == (0, 0) and self.bcpOut == (0, 0):
                    segment.type = "line"
                    segment.smooth = False
                else:
                    offCurves[-1].x = x
                    offCurves[-1].y = y
            elif value != (0, 0):
                segment.type = "curve"
                offCurves = segment.offCurve
                offCurves[-1].x = x
                offCurves[-1].y = y

    # bcp out

    bcpOut = dynamicProperty("base_bcpOut", "The outgoing off curve.")

    def _get_base_bcpOut(self):
        value = self._get_bcpOut()
        value = normalizers.normalizeCoordinateTuple(value)
        return value

    def _set_base_bcpOut(self, value):
        value = normalizers.normalizeCoordinateTuple(value)
        self._set_bcpOut(value)

    def _get_bcpOut(self):
        """
        Subclasses may override this method.
        """
        nextSegment = self._nextSegment
        offCurves = nextSegment.offCurve
        if offCurves:
            bcp = offCurves[0]
            x, y = relativeBCPOut(self.anchor, (bcp.x, bcp.y))
        else:
            x = y = 0
        return (x, y)

    def _set_bcpOut(self, value):
        """
        Subclasses may override this method.
        """
        x, y = absoluteBCPOut(self.anchor, value)
        segment = self._segment
        nextSegment = self._nextSegment
        if nextSegment.type == "move" and value != (0, 0):
            raise FontPartsError(("Cannot set the bcpOut for the last "
                                  "point in an open contour."))
        else:
            offCurves = nextSegment.offCurve
            if offCurves:
                # if the off curves are located at the anchor coordinates
                # we can switch to a "line" segment type
                if value == (0, 0) and self.bcpIn == (0, 0):
                    segment.type = "line"
                    segment.smooth = False
                else:
                    offCurves[0].x = x
                    offCurves[0].y = y
            elif value != (0, 0):
                nextSegment.type = "curve"
                offCurves = nextSegment.offCurve
                offCurves[0].x = x
                offCurves[0].y = y

    # type

    type = dynamicProperty("base_type", "The bPoint type.")

    def _get_base_type(self):
        value = self._get_type()
        value = normalizers.normalizeBPointType(value)
        return value

    def _set_base_type(self, value):
        value = normalizers.normalizeBPointType(value)
        self._set_type(value)

    def _get_type(self):
        """
        Subclasses may override this method.
        """
        point = self._point
        typ = point.type
        if typ == "curve" and point.smooth:
            bType = "curve"
        elif typ in ("move", "line", "curve"):
            bType = "corner"
        else:
            raise FontPartsError(
                "A %s point can not be converted to a bPoint." % typ)
        return bType

    def _set_type(self, value):
        """
        Subclasses may override this method.
        """
        point = self._point
        # convert corner to curve
        if value == "curve" and point.type == "line":
            # This needs to insert off curves without
            # generating unnecessary points in the
            # following segment. The segment object
            # implements this logic, so delegate the
            # change to the corresponding segment.
            segment = self._segment
            segment.type = "curve"
            segment.smooth = True
        # convert curve to corner
        elif value == "corner" and point.type == "curve":
            point.smooth = False

    # --------------
    # Identification
    # --------------

    index = dynamicProperty("index",
                            ("The index of the bPoint within the ordered "
                             "list of the parent contour's bPoints. None "
                             "if the bPoint does not belong to a contour."))

    def _get_base_index(self):
        if self.contour is None:
            return None
        value = self._get_index()
        value = normalizers.normalizeIndex(value)
        return value

    def _get_index(self):
        """
        Subclasses may override this method.
        """
        contour = self.contour
        value = contour.bPoints.index(self)
        return value

    # --------------
    # Transformation
    # --------------

    def _transformBy(self, matrix, **kwargs):
        """
        Subclasses may override this method.
        """
        anchor = self.anchor
        bcpIn = absoluteBCPIn(anchor, self.bcpIn)
        bcpOut = absoluteBCPOut(anchor, self.bcpOut)
        points = [bcpIn, anchor, bcpOut]
        t = transform.Transform(*matrix)
        bcpIn, anchor, bcpOut = t.transformPoints(points)
        x, y = anchor
        self._point.x = x
        self._point.y = y
        self.bcpIn = relativeBCPIn(anchor, bcpIn)
        self.bcpOut = relativeBCPOut(anchor, bcpOut)

    # ----
    # Misc
    # ----

    def round(self):
        """
        Round coordinates.
        """
        x, y = self.anchor
        self.anchor = (normalizers.normalizeRounding(x),
                       normalizers.normalizeRounding(y))
        x, y = self.bcpIn
        self.bcpIn = (normalizers.normalizeRounding(x),
                      normalizers.normalizeRounding(y))
        x, y = self.bcpOut
        self.bcpOut = (normalizers.normalizeRounding(x),
                       normalizers.normalizeRounding(y))
Esempio n. 13
0
class _BaseGlyphVendor(BaseObject, SelectionMixin, DeprecatedLayer,
                       RemovedLayer):
    """
    This class exists to provide common glyph
    interaction code to BaseFont and BaseLayer.
    It should not be directly subclassed.
    """

    # -----------------
    # Glyph Interaction
    # -----------------

    def _setLayerInGlyph(self, glyph):
        if glyph.layer is None:
            if isinstance(self, BaseLayer):
                layer = self
            else:
                layer = self.defaultLayer
            glyph.layer = layer

    def __len__(self):
        """
        An ``int`` representing number of glyphs in the layer. ::

            >>> len(layer)
            256
        """
        return self._len()

    def _len(self, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseLayer.__len__` and :meth:`BaseFont.__len__`
        This must return an ``int`` indicating
        the number of glyphs in the layer.

        Subclasses may override this method.
        """
        return len(self.keys())

    def __iter__(self):
        """
        Iterate through the :class:`BaseGlyph` objects in the layer. ::

            >>> for glyph in layer:
            ...     glyph.name
            "A"
            "B"
            "C"
        """
        return self._iter()

    def _iter(self, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseLayer.__iter__` and :meth:`BaseFont.__iter__`
        This must return an iterator that returns
        instances of a :class:`BaseGlyph` subclass.

        Subclasses may override this method.
        """
        for name in self.keys():
            yield self[name]

    def __getitem__(self, name):
        """
        Get the :class:`BaseGlyph` with name from the layer. ::

            >>> glyph = layer["A"]
        """
        name = normalizers.normalizeGlyphName(name)
        if name not in self:
            raise ValueError("No glyph named '%s'." % name)
        glyph = self._getItem(name)
        self._setLayerInGlyph(glyph)
        return glyph

    def _getItem(self, name, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseLayer.__getitem__` and :meth:`BaseFont.__getitem__`
        This must return an instance of a :class:`BaseGlyph`
        subclass. **name** will be a :ref:`type-string` representing
        a name of a glyph that is in the layer. It will have been
        normalized with :func:`normalizers.normalizeGlyphName`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def __setitem__(self, name, glyph):
        """
        Set the :class:`BaseGlyph` with name into the layer. ::

            >>> layer["A"] = glyph
        """
        self.insertGlyph(glyph, name)

    def keys(self):
        """
        Get a list of all glyphs in the layer. ::

            >>> layer.keys()
            ["B", "C", "A"]

        The order of the glyphs is undefined.
        """
        return self._keys()

    def _keys(self, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseLayer.keys` and :meth:`BaseFont.keys`
        This must return an :ref:`type-immutable-list`
        of the names representing all glyphs in the layer.
        The order is not defined.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def __contains__(self, name):
        """
        Test if the layer contains a glyph with **name**. ::

            >>> "A" in layer
            True
        """
        name = normalizers.normalizeGlyphName(name)
        return self._contains(name)

    def _contains(self, name, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseLayer.__contains__` and :meth:`BaseFont.__contains__`
        This must return ``bool`` indicating if the
        layer has a glyph with the defined name.
        **name** will be a :ref-type-string` representing
        a glyph name. It will have been normalized with
        :func:`normalizers.normalizeGlyphName`.

        Subclasses may override this method.
        """
        return name in self.keys()

    def newGlyph(self, name, clear=True):
        """
        Make a new glyph with **name** in the layer. ::

            >>> glyph = layer.newGlyph("A")

        The newly created :class:`BaseGlyph` will be returned.

        If the glyph exists in the layer and clear is set to ``False``,
        the existing glyph will be returned, otherwise the default
        behavior is to clear the exisiting glyph.
        """
        name = normalizers.normalizeGlyphName(name)
        if name not in self:
            glyph = self._newGlyph(name)
        elif clear:
            self.removeGlyph(name)
            glyph = self._newGlyph(name)
        else:
            glyph = self._getItem(name)
        self._setLayerInGlyph(glyph)
        return glyph

    def _newGlyph(self, name, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseLayer.newGlyph` and :meth:`BaseFont.newGlyph`
        This must return an instance of a :class:`BaseGlyph` subclass.
        **name** will be a :ref:`type-string` representing
        a glyph name. It will have been normalized with
        :func:`normalizers.normalizeGlyphName`. The
        name will have been tested to make sure that
        no glyph with the same name exists in the layer.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def removeGlyph(self, name):
        """
        Remove the glyph with name from the layer. ::

            >>> layer.removeGlyph("A")
        """
        name = normalizers.normalizeGlyphName(name)
        if name not in self:
            raise ValueError("No glyph with the name '%s' exists." % name)
        self._removeGlyph(name)

    def _removeGlyph(self, name, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseLayer.removeGlyph` and :meth:`BaseFont.removeGlyph`.
        **name** will be a :ref:`type-string` representing a
        glyph name of a glyph that is in the layer. It will
        have been normalized with :func:`normalizers.normalizeGlyphName`.
        The newly created :class:`BaseGlyph` must be returned.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def insertGlyph(self, glyph, name=None):
        """
        Insert **glyph** into the layer. ::

            >>> glyph = layer.insertGlyph(otherGlyph, name="A")

        This does not necessarily insert the glyph directly.
        In many cases, the environment will create a new
        glyph and copy the data from **glyph** to the new
        glyph. **name** indicates the name that should be
        assigned to the glyph after insertion. If **name**
        is not given, the glyph's original name must be used.
        If the glyph does not have a name, an error must be raised.
        The data that will be inserted from **glyph** is the
        same data as documented in :meth:`BaseGlyph.copy`.
        """
        if name is None:
            name = glyph.name
        name = normalizers.normalizeGlyphName(name)
        if name in self:
            self.removeGlyph(name)
        return self._insertGlyph(glyph, name=name)

    def _insertGlyph(self, glyph, name, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseLayer.insertGlyph` and :meth:`BaseFont.insertGlyph`.
        This must return an instance of a :class:`BaseGlyph` subclass.
        **glyph** will be a glyph object with the attributes necessary
        for copying as defined in :meth:`BaseGlyph.copy` An environment
        may choose to not insert **glyph** directly, opting to copy
        the data from **glyph** into a new glyph instead. **name**
        will be a :ref:`type-string` representing a glyph name. It
        will have been normalized with :func:`normalizers.normalizeGlyphName`.
        **name** will have been tested to make sure that no glyph with
        the same name exists in the layer.

        Subclasses may override this method.
        """
        dest = self.newGlyph(name)
        dest.copyData(glyph)
        return dest

    # ---------
    # Selection
    # ---------

    selectedGlyphs = dynamicProperty(
        "base_selectedGlyphs", """
        A list of glyphs selected in the layer.

        Getting selected glyph objects:

            >>> for glyph in layer.selectedGlyphs:
            ...     glyph.markColor = (1, 0, 0, 0.5)

        Setting selected glyph objects:

            >>> layer.selectedGlyphs = someGlyphs
        """)

    def _get_base_selectedGlyphs(self):
        selected = tuple([
            normalizers.normalizeGlyph(glyph)
            for glyph in self._get_selectedGlyphs()
        ])
        return selected

    def _get_selectedGlyphs(self):
        """
        Subclasses may override this method.
        """
        return self._getSelectedSubObjects(self)

    def _set_base_selectedGlyphs(self, value):
        normalized = [normalizers.normalizeGlyph(glyph) for glyph in value]
        self._set_selectedGlyphs(normalized)

    def _set_selectedGlyphs(self, value):
        """
        Subclasses may override this method.
        """
        return self._setSelectedSubObjects(self, value)

    selectedGlyphNames = dynamicProperty(
        "base_selectedGlyphNames", """
        A list of names of glyphs selected in the layer.

        Getting selected glyph names:

            >>> for name in layer.selectedGlyphNames:
            ...     print(name)

        Setting selected glyph names:

            >>> layer.selectedGlyphNames = ["A", "B", "C"]
        """)

    def _get_base_selectedGlyphNames(self):
        selected = tuple([
            normalizers.normalizeGlyphName(name)
            for name in self._get_selectedGlyphNames()
        ])
        return selected

    def _get_selectedGlyphNames(self):
        """
        Subclasses may override this method.
        """
        selected = [glyph.name for glyph in self.selectedGlyphs]
        return selected

    def _set_base_selectedGlyphNames(self, value):
        normalized = [normalizers.normalizeGlyphName(name) for name in value]
        self._set_selectedGlyphNames(normalized)

    def _set_selectedGlyphNames(self, value):
        """
        Subclasses may override this method.
        """
        select = [self[name] for name in value]
        self.selectedGlyphs = select

    # --------------------
    # Legacy Compatibility
    # --------------------

    has_key = __contains__
class GlyphCompatibilityReporter(BaseCompatibilityReporter):

    objectName = "Glyph"

    def __init__(self, glyph1, glyph2):
        super(GlyphCompatibilityReporter, self).__init__(glyph1, glyph2)
        self.contourCountDifference = False
        self.componentCountDifference = False
        self.guidelineCountDifference = False
        self.anchorDifferences = []
        self.anchorCountDifference = False
        self.anchorOrderDifference = False
        self.anchorsMissingFromGlyph1 = []
        self.anchorsMissingFromGlyph2 = []
        self.componentDifferences = []
        self.componentOrderDifference = False
        self.componentsMissingFromGlyph1 = []
        self.componentsMissingFromGlyph2 = []
        self.guidelinesMissingFromGlyph1 = []
        self.guidelinesMissingFromGlyph2 = []
        self.contours = []

    glyph1 = dynamicProperty("object1")
    glyph1Name = dynamicProperty("object1Name")
    glyph2 = dynamicProperty("object2")
    glyph2Name = dynamicProperty("object2Name")

    def report(self, showOK=True, showWarnings=True):
        glyph1 = self.glyph1
        glyph2 = self.glyph2
        report = []

        # Contour test
        if self.contourCountDifference:
            text = self.reportCountDifference(subObjectName="contours",
                                              object1Name=self.glyph1Name,
                                              object1Count=len(glyph1),
                                              object2Name=self.glyph2Name,
                                              object2Count=len(glyph2))
            report.append(self.formatFatalString(text))
        report += self.reportSubObjects(self.contours,
                                        showOK=showOK,
                                        showWarnings=showWarnings)

        # Component test
        if self.componentCountDifference:
            text = self.reportCountDifference(
                subObjectName="components",
                object1Name=self.glyph1Name,
                object1Count=len(glyph1.components),
                object2Name=self.glyph2Name,
                object2Count=len(glyph2.components))
            report.append(self.formatFatalString(text))
        elif self.componentOrderDifference:
            text = self.reportOrderDifference(
                subObjectName="components",
                object1Name=self.glyph1Name,
                object1Order=[c.baseGlyph for c in glyph1.components],
                object2Name=self.glyph2Name,
                object2Order=[c.baseGlyph for c in glyph2.components])
            report.append(self.formatWarningString(text))
        for name in self.componentsMissingFromGlyph2:
            text = self.reportDifferences(
                object1Name=self.glyph1Name,
                subObjectName="component",
                subObjectID=name,
                object2Name=self.glyph2Name,
            )
            report.append(self.formatWarningString(text))
        for name in self.componentsMissingFromGlyph1:
            text = self.reportDifferences(
                object1Name=self.glyph2Name,
                subObjectName="component",
                subObjectID=name,
                object2Name=self.glyph1Name,
            )
            report.append(self.formatWarningString(text))

        # Anchor test
        if self.anchorCountDifference:
            text = self.reportCountDifference(subObjectName="anchors",
                                              object1Name=self.glyph1Name,
                                              object1Count=len(glyph1.anchors),
                                              object2Name=self.glyph2Name,
                                              object2Count=len(glyph2.anchors))
            report.append(self.formatWarningString(text))
        elif self.anchorOrderDifference:
            text = self.reportOrderDifference(
                subObjectName="anchors",
                object1Name=self.glyph1Name,
                object1Order=[a.name for a in glyph1.anchors],
                object2Name=self.glyph2Name,
                object2Order=[a.name for a in glyph2.anchors])
            report.append(self.formatWarningString(text))
        for name in self.anchorsMissingFromGlyph2:
            text = self.reportDifferences(
                object1Name=self.glyph1Name,
                subObjectName="anchor",
                subObjectID=name,
                object2Name=self.glyph2Name,
            )
            report.append(self.formatWarningString(text))
        for name in self.anchorsMissingFromGlyph1:
            text = self.reportDifferences(
                object1Name=self.glyph2Name,
                subObjectName="anchor",
                subObjectID=name,
                object2Name=self.glyph1Name,
            )
            report.append(self.formatWarningString(text))

        # Guideline test
        if self.guidelineCountDifference:
            text = self.reportCountDifference(
                subObjectName="guidelines",
                object1Name=self.glyph1Name,
                object1Count=len(glyph1.guidelines),
                object2Name=self.glyph2Name,
                object2Count=len(glyph2.guidelines))
            report.append(self.formatWarningString(text))
        for name in self.guidelinesMissingFromGlyph2:
            text = self.reportDifferences(
                object1Name=self.glyph1Name,
                subObjectName="guideline",
                subObjectID=name,
                object2Name=self.glyph2Name,
            )
            report.append(self.formatWarningString(text))
        for name in self.guidelinesMissingFromGlyph1:
            text = self.reportDifferences(
                object1Name=self.glyph2Name,
                subObjectName="guideline",
                subObjectID=name,
                object2Name=self.glyph1Name,
            )
            report.append(self.formatWarningString(text))

        if report or showOK:
            report.insert(0, self.title)
        return "\n".join(report)
class FontCompatibilityReporter(BaseCompatibilityReporter):

    objectName = "Font"

    def __init__(self, font1, font2):
        super(FontCompatibilityReporter, self).__init__(font1, font2)
        self.guidelineCountDifference = False
        self.layerCountDifference = False
        self.guidelinesMissingFromFont2 = []
        self.guidelinesMissingInFont1 = []
        self.layersMissingFromFont2 = []
        self.layersMissingInFont1 = []
        self.layers = []

    font1 = dynamicProperty("object1")
    font1Name = dynamicProperty("object1Name")
    font2 = dynamicProperty("object2")
    font2Name = dynamicProperty("object2Name")

    def report(self, showOK=True, showWarnings=True):
        font1 = self.font1
        font2 = self.font2
        report = []
        if self.guidelineCountDifference:
            text = self.reportCountDifference(
                subObjectName="guidelines",
                object1Name=self.font1Name,
                object1Count=len(font1.guidelines),
                object2Name=self.font2Name,
                object2Count=len(font2.guidelines))
            report.append(self.formatWarningString(text))
        for name in self.guidelinesMissingFromFont2:
            text = self.reportDifferences(
                object1Name=self.font1Name,
                subObjectName="guideline",
                subObjectID=name,
                object2Name=self.font2Name,
            )
            report.append(self.formatWarningString(text))
        for name in self.guidelinesMissingInFont1:
            text = self.reportDifferences(
                object1Name=self.font2Name,
                subObjectName="guideline",
                subObjectID=name,
                object2Name=self.font1Name,
            )
            report.append(self.formatWarningString(text))
        if self.layerCountDifference:
            text = self.reportCountDifference(
                subObjectName="layers",
                object1Name=self.font1Name,
                object1Count=len(font1.layerOrder),
                object2Name=self.font2Name,
                object2Count=len(font2.layerOrder))
            report.append(self.formatWarningString(text))
        for name in self.layersMissingFromFont2:
            text = self.reportDifferences(
                object1Name=self.font1Name,
                subObjectName="layer",
                subObjectID=name,
                object2Name=self.font2Name,
            )
            report.append(self.formatWarningString(text))
        for name in self.layersMissingInFont1:
            text = self.reportDifferences(
                object1Name=self.font2Name,
                subObjectName="layer",
                subObjectID=name,
                object2Name=self.font1Name,
            )
            report.append(self.formatWarningString(text))
        report += self.reportSubObjects(self.layers,
                                        showOK=showOK,
                                        showWarnings=showWarnings)

        if report or showOK:
            report.insert(0, self.title)
        return "\n".join(report)
Esempio n. 16
0
class BaseFeatures(BaseObject, DeprecatedFeatures, RemovedFeatures):

    copyAttributes = ("text", )

    def _reprContents(self):
        contents = []
        if self.font is not None:
            contents.append("for font")
            contents += self.font._reprContents()
        return contents

    # -------
    # Parents
    # -------

    # Font

    _font = None

    font = dynamicProperty("font", "The features' parent :class:`BaseFont`.")

    def _get_font(self):
        if self._font is None:
            return None
        return self._font()

    def _set_font(self, font):
        if self._font is not None and self._font() != font:
            raise AssertionError(
                "font for features already set and is not same as font")
        if font is not None:
            font = reference(font)
        self._font = font

    # ----
    # Text
    # ----

    text = dynamicProperty(
        "base_text", """
        The `.fea formated
        <http://www.adobe.com/devnet/opentype/afdko/topic_feature_file_syntax.html>`_
        text representing the features.
        It must be a :ref:`type-string`.
        """)

    def _get_base_text(self):
        value = self._get_text()
        if value is not None:
            value = normalizers.normalizeFeatureText(value)
        return value

    def _set_base_text(self, value):
        if value is not None:
            value = normalizers.normalizeFeatureText(value)
        self._set_text(value)

    def _get_text(self):
        """
        This is the environment implementation of
        :attr:`BaseFeatures.text`. This must return a
        :ref:`type-string`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_text(self, value):
        """
        This is the environment implementation of
        :attr:`BaseFeatures.text`. **value** will be
        a :ref:`type-string`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()
Esempio n. 17
0
class BaseGroups(BaseDict, DeprecatedGroups, RemovedGroups):
    """
    A Groups object. This object normally created as part of a
    :class:`BaseFont`. An orphan Groups object can be created like this::

        >>> groups = RGroups()

    This object behaves like a Python dictionary. Most of the dictionary
    functionality comes from :class:`BaseDict`, look at that object for the
    required environment implementation details.

    Groups uses :func:`normalizers.normalizeGroupKey` to normalize the key of
    the ``dict``, and :func:`normalizers.normalizeGroupValue` to normalize the
    value of the ``dict``.
    """

    keyNormalizer = normalizers.normalizeGroupKey
    valueNormalizer = normalizers.normalizeGroupValue

    def _reprContents(self):
        contents = []
        if self.font is not None:
            contents.append("for font")
            contents += self.font._reprContents()
        return contents

    # -------
    # Parents
    # -------

    # Font

    _font = None

    font = dynamicProperty("font", "The Groups' parent :class:`BaseFont`.")

    def _get_font(self):
        if self._font is None:
            return None
        return self._font()

    def _set_font(self, font):
        assert self._font is None or self._font() == font
        if font is not None:
            font = reference(font)
        self._font = font

    # ---------
    # Searching
    # ---------

    def findGlyph(self, glyphName):
        """
        Returns a ``list`` of the group or groups associated with
        **glyphName**.
        **glyphName** will be an :ref:`type-string`. If no group is found
        to contain **glyphName** an empty ``list`` will be returned. ::

            >>> font.groups.findGlyph("A")
            ["A_accented"]
        """
        glyphName = normalizers.normalizeGlyphName(glyphName)
        groupNames = self._findGlyph(glyphName)
        groupNames = [
            self.keyNormalizer.__func__(groupName) for groupName in groupNames
        ]
        return groupNames

    def _findGlyph(self, glyphName):
        """
        This is the environment implementation of
        :meth:`BaseGroups.findGlyph`. **glyphName** will be
        an :ref:`type-string`.

        Subclasses may override this method.
        """
        found = []
        for key, groupList in self.items():
            if glyphName in groupList:
                found.append(key)
        return found

    # --------------
    # Kerning Groups
    # --------------

    side1KerningGroups = dynamicProperty(
        "base_side1KerningGroups", """
        All groups marked as potential side 1
        kerning members.

            >>> side1Groups = groups.side1KerningGroups

        The value will be a :ref:`dict` with
        :ref:`string` keys representing group names
        and :ref:`tuple` contaning glyph names.
        """)

    def get_base_side1KerningGroups(self):
        kerningGroups = self._get_side1KerningGroups()
        normalized = {}
        for name, members in kerningGroups.items():
            name = normalizers.normalizeGroupKey(name)
            members = normalizers.normalizeGroupValue(members)
        return normalized

    def _get_base_side1KerningGroups(self):
        """
        Subclasses may override this method.
        """
        found = {}
        for name, contents in self.items():
            if name.startswith("public.kern1."):
                found[name] = contents
        return found

    side2KerningGroups = dynamicProperty(
        "base_side2KerningGroups", """
        All groups marked as potential side 1
        kerning members.

            >>> side2Groups = groups.side2KerningGroups

        The value will be a :ref:`dict` with
        :ref:`string` keys representing group names
        and :ref:`tuple` contaning glyph names.
        """)

    def _get_base_side2KerningGroups(self):
        kerningGroups = self._get_side2KerningGroups()
        normalized = {}
        for name, members in kerningGroups.items():
            name = normalizers.normalizeGroupKey(name)
            members = normalizers.normalizeGroupValue(members)
        return normalized

    def get_base_side2KerningGroups(self):
        """
        Subclasses may override this method.
        """
        found = {}
        for name, contents in self.items():
            if name.startswith("public.kern2."):
                found[name] = contents
        return found

    # ---------------------
    # RoboFab Compatibility
    # ---------------------

    def remove(self, groupName):
        """
        Removes a group from the Groups. **groupName** will be
        a :ref:`type-string` that is the group name to
        be removed.

        This is a backwards compatibility method.
        """
        del self[groupName]

    def asDict(self):
        """
        Return the Groups as a ``dict``.

        This is a backwards compatibility method.
        """
        d = {}
        for k, v in self.items():
            d[k] = v
        return d

    # -------------------
    # Inherited Functions
    # -------------------

    def __contains__(self, groupName):
        """
        Tests to see if a group name is in the Groups.
        **groupName** will be a :ref:`type-string`.
        This returns a ``bool`` indicating if the **groupName**
        is in the Groups. ::

            >>> "myGroup" in font.groups
            True
        """
        return super(BaseGroups, self).__contains__(groupName)

    def __delitem__(self, groupName):
        """
        Removes **groupName** from the Groups. **groupName** is a
        :ref:`type-string`.::

            >>> del font.groups["myGroup"]
        """
        super(BaseGroups, self).__delitem__(groupName)

    def __getitem__(self, groupName):
        """
        Returns the contents of the named group. **groupName** is a
        :ref:`type-string`.
        The returned value will be a ``list`` of the group contents.::

            >>> font.groups["myGroup"]
            ["A", "B", "C"]

        It is important to understand that any changes to the returned group
        contents will not be reflected in the Groups object. If one wants to
        make a change to the group contents, one should do the following::

            >>> group = font.groups["myGroup"]
            >>> group.remove("A")
            >>> font.groups["myGroup"] = group
        """
        return super(BaseGroups, self).__getitem__(groupName)

    def __iter__(self):
        """
        Iterates through the Groups, giving the key for each iteration. The
        order that the Groups will iterate though is not fixed nor is it
        ordered.::

            >>> for groupName in font.groups:
            >>>     print groupName
            "myGroup"
            "myGroup3"
            "myGroup2"
        """
        return super(BaseGroups, self).__iter__()

    def __len__(self):
        """
        Returns the number of groups in Groups as an ``int``.::

            >>> len(font.groups)
            5
        """
        return super(BaseGroups, self).__len__()

    def __setitem__(self, groupName, glyphNames):
        """
        Sets the **groupName** to the list of **glyphNames**. **groupName**
        is the group name as a :ref:`type-string` and **glyphNames** is a
        ``list`` of glyph names as :ref:`type-string`.

            >>> font.groups["myGroup"] = ["A", "B", "C"]
        """
        super(BaseGroups, self).__setitem__(groupName, glyphNames)

    def clear(self):
        """
        Removes all group information from Groups,
        resetting the Groups to an empty dictionary. ::

            >>> font.groups.clear()
        """
        super(BaseGroups, self).clear()

    def get(self, groupName, default=None):
        """
        Returns the contents of the named group.
        **groupName** is a :ref:`type-string`, and the returned values will
        either be ``list`` of group contents or ``None`` if no group was
        found. ::

            >>> font.groups["myGroup"]
            ["A", "B", "C"]

        It is important to understand that any changes to the returned group
        contents will not be reflected in the Groups object. If one wants to
        make a change to the group contents, one should do the following::

            >>> group = font.groups["myGroup"]
            >>> group.remove("A")
            >>> font.groups["myGroup"] = group
        """
        return super(BaseGroups, self).get(groupName, default)

    def items(self):
        """
        Returns a list of ``tuple`` of each group name and group members.
        Group names are :ref:`type-string` and group members are a ``list``
        of :ref:`type-string`. The initial list will be unordered.

            >>> font.groups.items()
            [("myGroup", ["A", "B", "C"]), ("myGroup2", ["D", "E", "F"])]
        """
        return super(BaseGroups, self).items()

    def keys(self):
        """
        Returns a ``list`` of all the group names in Groups. This list will be
        unordered.::

            >>> font.groups.keys()
            ["myGroup4", "myGroup1", "myGroup5"]
        """
        return super(BaseGroups, self).keys()

    def pop(self, groupName, default=None):
        """
        Removes the **groupName** from the Groups and returns the ``list`` of
        group members. If no group is found, **default** is returned.
        **groupName** is a :ref:`type-string`. This must return either
        **default** or a ``list`` of glyph names as :ref:`type-string`.

            >>> font.groups.pop("myGroup")
            ["A", "B", "C"]
        """
        return super(BaseGroups, self).pop(groupName, default)

    def update(self, otherGroups):
        """
        Updates the Groups based on **otherGroups**. *otherGroups** is a
        ``dict`` of groups information. If a group from **otherGroups** is in
        Groups, the group members will be replaced by the group members from
        **otherGroups**. If a group from **otherGroups** is not in the Groups,
        it is added to the Groups. If Groups contain a group name that is not
        in *otherGroups**, it is not changed.

            >>> font.groups.update(newGroups)
        """
        super(BaseGroups, self).update(otherGroups)

    def values(self):
        """
        Returns a ``list`` of each named group's members. This will be a list
        of lists, the group members will be a ``list`` of :ref:`type-string`.
        The initial list will be unordered.

            >>> font.groups.items()
            [["A", "B", "C"], ["D", "E", "F"]]
        """
        return super(BaseGroups, self).values()
Esempio n. 18
0
class BaseComponent(BaseObject, TransformationMixin, InterpolationMixin,
                    SelectionMixin, IdentifierMixin, DeprecatedComponent,
                    RemovedComponent):

    copyAttributes = ("baseGlyph", "transformation")

    def _reprContents(self):
        contents = [
            "baseGlyph='%s'" % self.baseGlyph,
            "offset='({x}, {y})'".format(x=self.offset[0], y=self.offset[1]),
        ]
        if self.glyph is not None:
            contents.append("in glyph")
            contents += self.glyph._reprContents()
        return contents

    # -------
    # Parents
    # -------

    # Glyph

    _glyph = None

    glyph = dynamicProperty("glyph", "The component's parent glyph.")

    def _get_glyph(self):
        if self._glyph is None:
            return None
        return self._glyph()

    def _set_glyph(self, glyph):
        if self._glyph is not None:
            raise AssertionError("glyph for component already set")
        if glyph is not None:
            glyph = reference(glyph)
        self._glyph = glyph

    # Layer

    layer = dynamicProperty("layer", "The component's parent layer.")

    def _get_layer(self):
        if self._glyph is None:
            return None
        return self.glyph.layer

    # Font

    font = dynamicProperty("font", "The component's parent font.")

    def _get_font(self):
        if self._glyph is None:
            return None
        return self.glyph.font

    # ----------
    # Attributes
    # ----------

    # baseGlyph

    baseGlyph = dynamicProperty("base_baseGlyph",
                                "The glyph the component references.")

    def _get_base_baseGlyph(self):
        value = self._get_baseGlyph()
        # if the component does not belong to a layer,
        # it is allowed to have None as its baseGlyph
        if value is None and self.layer is None:
            pass
        else:
            value = normalizers.normalizeGlyphName(value)
        return value

    def _set_base_baseGlyph(self, value):
        value = normalizers.normalizeGlyphName(value)
        self._set_baseGlyph(value)

    def _get_baseGlyph(self):
        """
        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_baseGlyph(self, value):
        """
        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # transformation

    transformation = dynamicProperty("base_transformation",
                                     "The component's transformation matrix.")

    def _get_base_transformation(self):
        value = self._get_transformation()
        value = normalizers.normalizeTransformationMatrix(value)
        return value

    def _set_base_transformation(self, value):
        value = normalizers.normalizeTransformationMatrix(value)
        self._set_transformation(value)

    def _get_transformation(self):
        """
        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_transformation(self, value):
        """
        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # offset

    offset = dynamicProperty("base_offset", "The component's offset.")

    def _get_base_offset(self):
        value = self._get_offset()
        value = normalizers.normalizeTransformationOffset(value)
        return value

    def _set_base_offset(self, value):
        value = normalizers.normalizeTransformationOffset(value)
        self._set_offset(value)

    def _get_offset(self):
        """
        Subclasses may override this method.
        """
        sx, sxy, syx, sy, ox, oy = self.transformation
        return ox, oy

    def _set_offset(self, value):
        """
        Subclasses may override this method.
        """
        sx, sxy, syx, sy, ox, oy = self.transformation
        ox, oy = value
        self.transformation = sx, sxy, syx, sy, ox, oy

    # scale

    scale = dynamicProperty("base_scale", "The component's scale.")

    def _get_base_scale(self):
        value = self._get_scale()
        value = normalizers.normalizeComponentScale(value)
        return value

    def _set_base_scale(self, value):
        value = normalizers.normalizeComponentScale(value)
        self._set_scale(value)

    def _get_scale(self):
        """
        Subclasses may override this method.
        """
        sx, sxy, syx, sy, ox, oy = self.transformation
        return sx, sy

    def _set_scale(self, value):
        """
        Subclasses may override this method.
        """
        sx, sxy, syx, sy, ox, oy = self.transformation
        sx, sy = value
        self.transformation = sx, sxy, syx, sy, ox, oy

    # --------------
    # Identification
    # --------------

    # index

    index = dynamicProperty("base_index",
                            ("The index of the component within the "
                             "ordered list of the parent glyph's components."))

    def _get_base_index(self):
        glyph = self.glyph
        if glyph is None:
            return None
        value = self._get_index()
        value = normalizers.normalizeIndex(value)
        return value

    def _set_base_index(self, value):
        glyph = self.glyph
        if glyph is None:
            raise FontPartsError("The component does not belong to a glyph.")
        value = normalizers.normalizeIndex(value)
        componentCount = len(glyph.components)
        if value < 0:
            value = -(value % componentCount)
        if value >= componentCount:
            value = componentCount
        self._set_index(value)

    def _get_index(self):
        """
        Subclasses may override this method.
        """
        glyph = self.glyph
        return glyph.components.index(self)

    def _set_index(self, value):
        """
        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # ----
    # Pens
    # ----

    def draw(self, pen):
        """
        Draw the component with the given Pen.
        """
        self._draw(pen)

    def _draw(self, pen, **kwargs):
        """
        Subclasses may override this method.
        """
        from fontTools.ufoLib.pointPen import PointToSegmentPen
        adapter = PointToSegmentPen(pen)
        self.drawPoints(adapter)

    def drawPoints(self, pen):
        """
        Draw the contour with the given PointPen.
        """
        self._drawPoints(pen)

    def _drawPoints(self, pen, **kwargs):
        """
        Subclasses may override this method.
        """
        # The try: ... except TypeError: ...
        # handles backwards compatibility with
        # point pens that have not been upgraded
        # to point pen protocol 2.
        try:
            pen.addComponent(self.baseGlyph,
                             self.transformation,
                             identifier=self.identifier,
                             **kwargs)
        except TypeError:
            pen.addComponent(self.baseGlyph, self.transformation, **kwargs)

    # --------------
    # Transformation
    # --------------

    def _transformBy(self, matrix, **kwargs):
        """
        Subclasses may override this method.
        """
        t = transform.Transform(*matrix)
        transformation = t.transform(self.transformation)
        self.transformation = tuple(transformation)

    # -------------
    # Normalization
    # -------------

    def round(self):
        """
        Round offset coordinates.
        """
        self._round()

    def _round(self):
        """
        Subclasses may override this method.
        """
        x, y = self.offset
        x = normalizers.normalizeVisualRounding(x)
        y = normalizers.normalizeVisualRounding(y)
        self.offset = (x, y)

    def decompose(self):
        """
        Decompose the component.
        """
        glyph = self.glyph
        if glyph is None:
            raise FontPartsError("The component does not belong to a glyph.")
        self._decompose()

    def _decompose(self):
        """
        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # -------------
    # Interpolation
    # -------------

    compatibilityReporterClass = ComponentCompatibilityReporter

    def isCompatible(self, other):
        """
        Evaluate interpolation compatibility with **other**. ::

            >>> compatible, report = self.isCompatible(otherComponent)
            >>> compatible
            True
            >>> compatible
            [Warning] Component: "A" + "B"
            [Warning] Component: "A" has name A | "B" has name B

        This will return a ``bool`` indicating if the component is
        compatible for interpolation with **other** and a
        :ref:`type-string` of compatibility notes.
        """
        return super(BaseComponent, self).isCompatible(other, BaseComponent)

    def _isCompatible(self, other, reporter):
        """
        This is the environment implementation of
        :meth:`BaseComponent.isCompatible`.

        Subclasses may override this method.
        """
        component1 = self
        component2 = other
        # base glyphs
        if component1.baseName != component2.baseName:
            reporter.baseDifference = True
            reporter.warning = True

    # ------------
    # Data Queries
    # ------------

    def pointInside(self, point):
        """
        Determine if point is in the black or white of the component.

        point must be an (x, y) tuple.
        """
        point = normalizers.normalizeCoordinateTuple(point)
        return self._pointInside(point)

    def _pointInside(self, point):
        """
        Subclasses may override this method.
        """
        from fontTools.pens.pointInsidePen import PointInsidePen
        pen = PointInsidePen(glyphSet=self.layer,
                             testPoint=point,
                             evenOdd=False)
        self.draw(pen)
        return pen.getResult()

    bounds = dynamicProperty("base_bounds",
                             ("The bounds of the component: "
                              "(xMin, yMin, xMax, yMax) or None."))

    def _get_base_bounds(self):
        value = self._get_bounds()
        if value is not None:
            value = normalizers.normalizeBoundingBox(value)
        return value

    def _get_bounds(self):
        """
        Subclasses may override this method.
        """
        from fontTools.pens.boundsPen import BoundsPen
        pen = BoundsPen(self.layer)
        self.draw(pen)
        return pen.bounds
Esempio n. 19
0
class BaseLayer(_BaseGlyphVendor, InterpolationMixin):
    def _reprContents(self):
        contents = [
            "'%s'" % self.name,
        ]
        if self.color:
            contents.append("color=%r" % str(self.color))
        return contents

    # ----
    # Copy
    # ----

    copyAttributes = ("name", "color", "lib")

    def copy(self):
        """
        Copy the layer into a new layer that does not
        belong to a font. ::

            >>> copiedLayer = layer.copy()

        This will copy:

        * name
        * color
        * lib
        * glyphs
        """
        return super(BaseLayer, self).copy()

    def copyData(self, source):
        """
        Copy data from **source** into this layer.
        Refer to :meth:`BaseLayer.copy` for a list
        of values that will be copied.
        """
        super(BaseLayer, self).copyData(source)
        for name in source.keys():
            glyph = self.newGlyph(name)
            glyph.copyData(source[name])

    # -------
    # Parents
    # -------

    # Font

    _font = None

    font = dynamicProperty(
        "font", """
        The layer's parent :class:`BaseFont`. ::

            >>> font = layer.font
        """)

    def _get_font(self):
        if self._font is None:
            return None
        return self._font()

    def _set_font(self, font):
        assert self._font is None
        if font is not None:
            font = reference(font)
        self._font = font

    # --------------
    # Identification
    # --------------

    # name

    name = dynamicProperty(
        "base_name", """
        The name of the layer. ::

            >>> layer.name
            "foreground"
            >>> layer.name = "top"
        """)

    def _get_base_name(self):
        value = self._get_name()
        if value is not None:
            value = normalizers.normalizeLayerName(value)
        return value

    def _set_base_name(self, value):
        if value == self.name:
            return
        value = normalizers.normalizeLayerName(value)
        existing = self.font.layerOrder
        if value in existing:
            raise ValueError("A layer with the name '%s' already exists." %
                             value)
        self._set_name(value)

    def _get_name(self):
        """
        This is the environment implementation of :attr:`BaseLayer.name`.
        This must return a :ref:`type-string` defining the name of the
        layer. If the layer is the default layer, the returned value
        must be ``None``. It will be normalized with
        :func:`normalizers.normalizeLayerName`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_name(self, value, **kwargs):
        """
        This is the environment implementation of :attr:`BaseLayer.name`.
        **value** will be a :ref:`type-string` defining the name of the
        layer. It will have been normalized with
        :func:`normalizers.normalizeLayerName`.
        No layer with the same name will exist.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # color

    color = dynamicProperty(
        "base_color", """
        The layer's color. ::

            >>> layer.color
            None
            >>> layer.color = (1, 0, 0, 0.5)
        """)

    def _get_base_color(self):
        value = self._get_color()
        if value is not None:
            value = normalizers.normalizeColor(value)
            value = Color(value)
        return value

    def _set_base_color(self, value):
        if value is not None:
            value = normalizers.normalizeColor(value)
        self._set_color(value)

    def _get_color(self):
        """
        This is the environment implementation of :attr:`BaseLayer.color`.
        This must return a :ref:`type-color` defining the
        color assigned to the layer. If the layer does not
        have an assigned color, the returned value must be
        ``None``. It will be normalized with
        :func:`normalizers.normalizeColor`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_color(self, value, **kwargs):
        """
        This is the environment implementation of :attr:`BaseLayer.color`.
        **value** will be a :ref:`type-color` or ``None`` defining the
        color to assign to the layer. It will have been normalized with
        :func:`normalizers.normalizeColor`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # -----------
    # Sub-Objects
    # -----------

    # lib

    lib = dynamicProperty(
        "lib", """
        The layer's :class:`BaseLib` object. ::

            >>> layer.lib["org.robofab.hello"]
            "world"
        """)

    def _get_base_lib(self):
        lib = self._get_lib()
        lib.font = self
        return lib

    def _get_lib(self):
        """
        This is the environment implementation of :attr:`BaseLayer.lib`.
        This must return an instance of a :class:`BaseLib` subclass.
        """
        self.raiseNotImplementedError()

    # -----------------
    # Global Operations
    # -----------------

    def round(self):
        """
        Round all approriate data to integers. ::

            >>> layer.round()

        This is the equivalent of calling the round method on:

        * all glyphs in the layer
        """
        self._round()

    def _round(self):
        """
        This is the environment implementation of :meth:`BaseLayer.round`.

        Subclasses may override this method.
        """
        for glyph in self:
            glyph.round()

    def autoUnicodes(self):
        """
        Use heuristics to set Unicode values in all glyphs. ::

            >>> layer.autoUnicodes()

        Environments will define their own heuristics for
        automatically determining values.
        """
        self._autoUnicodes()

    def _autoUnicodes(self):
        """
        This is the environment implementation of
        :meth:`BaseLayer.autoUnicodes`.

        Subclasses may override this method.
        """
        for glyph in self:
            glyph.autoUnicodes()

    # -------------
    # Interpolation
    # -------------

    def interpolate(self,
                    factor,
                    minLayer,
                    maxLayer,
                    round=True,
                    suppressError=True):
        """
        Interpolate all possible data in the layer. ::

            >>> layer.interpolate(0.5, otherLayer1, otherLayer2)
            >>> layer.interpolate((0.5, 2.0), otherLayer1, otherLayer2, round=False)

        The interpolation occurs on a 0 to 1.0 range where **minLayer**
        is located at 0 and **maxLayer** is located at 1.0. **factor**
        is the interpolation value. It may be less than 0 and greater
        than 1.0. It may be a :ref:`type-int-float` or a tuple of
        two :ref:`type-int-float`. If it is a tuple, the first
        number indicates the x factor and the second number indicates
        the y factor. **round** indicates if the result should be
        rounded to integers. **suppressError** indicates if incompatible
        data should be ignored or if an error should be raised when
        such incompatibilities are found.
        """
        factor = normalizers.normalizeInterpolationFactor(factor)
        if not isinstance(minLayer, BaseLayer):
            raise TypeError(
                ("Interpolation to an instance of %r can not be "
                 "performed from an instance of %r.") %
                (self.__class__.__name__, minLayer.__class__.__name__))
        if not isinstance(maxLayer, BaseLayer):
            raise TypeError(
                ("Interpolation to an instance of %r can not be "
                 "performed from an instance of %r.") %
                (self.__class__.__name__, maxLayer.__class__.__name__))
        round = normalizers.normalizeBoolean(round)
        suppressError = normalizers.normalizeBoolean(suppressError)
        self._interpolate(factor,
                          minLayer,
                          maxLayer,
                          round=round,
                          suppressError=suppressError)

    def _interpolate(self,
                     factor,
                     minLayer,
                     maxLayer,
                     round=True,
                     suppressError=True):
        """
        This is the environment implementation of
        :meth:`BaseLayer.interpolate`.

        Subclasses may override this method.
        """
        for glyphName in self.keys():
            del self[glyphName]
        for glyphName in minLayer.keys():
            if glyphName not in maxLayer:
                continue
            minGlyph = minLayer[glyphName]
            maxGlyph = maxLayer[glyphName]
            dstGlyph = self.newGlyph(glyphName)
            dstGlyph.interpolate(factor,
                                 minGlyph,
                                 maxGlyph,
                                 round=round,
                                 suppressError=suppressError)

    compatibilityReporterClass = LayerCompatibilityReporter

    def isCompatible(self, other):
        """
        Evaluate interpolation compatibility with **other**. ::

            >>> compat, report = self.isCompatible(otherLayer)
            >>> compat
            False
            >>> report
            A
            -
            [Fatal] The glyphs do not contain the same number of contours.

        This will return a ``bool`` indicating if the layer is
        compatible for interpolation with **other** and a
        :ref:`type-string` of compatibility notes.
        """
        return super(BaseLayer, self).isCompatible(other, BaseLayer)

    def _isCompatible(self, other, reporter):
        """
        This is the environment implementation of
        :meth:`BaseLayer.isCompatible`.

        Subclasses may override this method.
        """
        layer1 = self
        layer2 = other

        # incompatible number of glyphs
        glyphs1 = set(layer1.keys())
        glyphs2 = set(layer2.keys())
        if len(glyphs1) != len(glyphs2):
            reporter.glyphCountDifference = True
            reporter.warning = True
        if len(glyphs1.difference(glyphs2)) != 0:
            reporter.warning = True
            reporter.glyphsMissingFromLayer2 = list(
                glyphs1.difference(glyphs2))
        if len(glyphs2.difference(glyphs1)) != 0:
            reporter.warning = True
            reporter.glyphsMissingInLayer1 = list(glyphs2.difference(glyphs1))
        # test glyphs
        for glyphName in sorted(glyphs1.intersection(glyphs2)):
            glyph1 = layer1[glyphName]
            glyph2 = layer2[glyphName]
            glyphCompatibility = glyph1.isCompatible(glyph2)[1]
            if glyphCompatibility.fatal or glyphCompatibility.warning:
                if glyphCompatibility.fatal:
                    reporter.fatal = True
                if glyphCompatibility.warning:
                    reporter.warning = True
                reporter.glyphs.append(glyphCompatibility)

    # -------
    # mapping
    # -------

    def getReverseComponentMapping(self):
        """
        Create a dictionary of unicode -> [glyphname, ...] mappings.
        All glyphs are loaded. Note that one glyph can have multiple
        unicode values, and a unicode value can have multiple glyphs
        pointing to it.
        """
        return self._getReverseComponentMapping()

    def _getReverseComponentMapping(self):
        """
        This is the environment implementation of
        :meth:`BaseFont.getReverseComponentMapping`.

        Subclasses may override this method.
        """
        self.raiseNotImplementedError()

    def getCharacterMapping(self):
        """
        Get a reversed map of component references in the font.
        {
        'A' : ['Aacute', 'Aring']
        'acute' : ['Aacute']
        'ring' : ['Aring']
        etc.
        }
        """
        return self._getCharacterMapping()

    def _getCharacterMapping(self):
        """
        This is the environment implementation of
        :meth:`BaseFont.getCharacterMapping`.

        Subclasses may override this method.
        """
        self.raiseNotImplementedError()
Esempio n. 20
0
class BaseKerning(BaseDict, DeprecatedKerning, RemovedKerning):
    """
    A Kerning object. This object normally created as part of a
    :class:`BaseFont`. An orphan Kerning object can be created
    like this::

        >>> groups = RKerning()

    This object behaves like a Python dictionary. Most of the
    dictionary functionality comes from :class:`BaseDict`, look at
    that object for the required environment implementation details.

    Kerning uses :func:`normalizers.normalizeKerningKey` to normalize the
    key of the ``dict``, and :func:`normalizers.normalizeKerningValue`
    to normalize the the value of the ``dict``.
    """

    keyNormalizer = normalizers.normalizeKerningKey
    valueNormalizer = normalizers.normalizeKerningValue

    def _reprContents(self):
        contents = []
        if self.font is not None:
            contents.append("for font")
            contents += self.font._reprContents()
        return contents

    # -------
    # Parents
    # -------

    # Font

    _font = None

    font = dynamicProperty("font", "The Kerning's parent :class:`BaseFont`.")

    def _get_font(self):
        if self._font is None:
            return None
        return self._font()

    def _set_font(self, font):
        assert self._font is None or self._font() == font
        if font is not None:
            font = reference(font)
        self._font = font

    # --------------
    # Transformation
    # --------------

    def scaleBy(self, factor):
        """
        Scales all kerning values by **factor**. **factor** will be an
        :ref:`type-int-float`, ``tuple`` or ``list``. The first value of the
        **factor** will be used to scale the kerning values.

            >>> myKerning.scaleBy(2)
            >>> myKerning.scaleBy((2,3))
        """
        factor = normalizers.normalizeTransformationScale(factor)
        self._scale(factor)

    def _scale(self, factor):
        """
        This is the environment implementation of :meth:`BaseKerning.scaleBy`.
        **factor** will be a ``tuple``.

        Subclasses may override this method.
        """
        factor = factor[0]
        for k, v in self.items():
            v *= factor
            self[k] = v

    # -------------
    # Normalization
    # -------------

    def round(self, multiple=1):
        """
        Rounds the kerning values to increments of **multiple**,
        which will be an ``int``.

        The default behavior is to round to increments of 1.
        """
        if not isinstance(multiple, int):
            raise TypeError("The round multiple must be an int not %s." %
                            multiple.__class__.__name__)
        self._round(multiple)

    def _round(self, multiple=1):
        """
        This is the environment implementation of
        :meth:`BaseKerning.round`. **multiple** will be an ``int``.

        Subclasses may override this method.
        """
        for pair, value in self.items():
            value = int(normalizers.normalizeRounding(
                value / float(multiple))) * multiple
            self[pair] = value

    # -------------
    # Interpolation
    # -------------

    def interpolate(self,
                    factor,
                    minKerning,
                    maxKerning,
                    round=True,
                    suppressError=True):
        """
        Interpolates all pairs between two :class:`BaseKerning` objects:

        **minKerning** and **maxKerning**. The interpolation occurs on a
        0 to 1.0 range where **minKerning** is located at 0 and
        **maxKerning** is located at 1.0. The kerning data is replaced by
        the interpolated kerning.

        * **factor** is the interpolation value. It may be less than 0
          and greater than 1.0. It may be an :ref:`type-int-float`,
          ``tuple`` or ``list``. If it is a ``tuple`` or ``list``,
          the first number indicates the x factor and the second number
          indicates the y factor.
        * **round** is a ``bool`` indicating if the result should be rounded to
          ``int``\s. The default behavior is to round interpolated kerning.
        * **suppressError** is a ``bool`` indicating if incompatible data should
          be ignored or if an error should be raised when such incompatibilities
          are found. The default behavior is to ignore incompatible data.

            >>> myKerning.interpolate(kerningOne, kerningTwo)
        """
        factor = normalizers.normalizeInterpolationFactor(factor)
        if not isinstance(minKerning, BaseKerning):
            raise TypeError(
                ("Interpolation to an instance of %r can not be "
                 "performed from an instance of %r.") %
                (self.__class__.__name__, minKerning.__class__.__name__))
        if not isinstance(maxKerning, BaseKerning):
            raise TypeError(
                ("Interpolation to an instance of %r can not be "
                 "performed from an instance of %r.") %
                (self.__class__.__name__, maxKerning.__class__.__name__))
        round = normalizers.normalizeBoolean(round)
        suppressError = normalizers.normalizeBoolean(suppressError)
        self._interpolate(factor,
                          minKerning,
                          maxKerning,
                          round=round,
                          suppressError=suppressError)

    def _interpolate(self,
                     factor,
                     minKerning,
                     maxKerning,
                     round=True,
                     suppressError=True):
        """
        This is the environment implementation of :meth:`BaseKerning.interpolate`.

        * **factor** will be an :ref:`type-int-float`, ``tuple`` or ``list``.
        * **minKerning** will be a :class:`BaseKerning` object.
        * **maxKerning** will be a :class:`BaseKerning` object.
        * **round** will be a ``bool`` indicating if the interpolated kerning
          should be rounded.
        * **suppressError** will be a ``bool`` indicating if incompatible data
          should be ignored.

        Subclasses may override this method.
        """
        import fontMath
        minKerning = fontMath.MathKerning(kerning=minKerning,
                                          groups=minKerning.font.groups)
        maxKerning = fontMath.MathKerning(kerning=maxKerning,
                                          groups=maxKerning.font.groups)
        result = interpolate(minKerning, maxKerning, factor)
        if round:
            result.round()
        self.clear()
        result.extractKerning(self.font)

    # ---------------------
    # RoboFab Compatibility
    # ---------------------

    def remove(self, pair):
        """
        Removes a pair from the Kerning. **pair** will
        be a ``tuple`` of two :ref:`type-string`\s.

        This is a backwards compatibility method.
        """
        del self[pair]

    def asDict(self, returnIntegers=True):
        """
        Return the Kerning as a ``dict``.

        This is a backwards compatibility method.
        """
        d = {}
        for k, v in self.items():
            d[k] = v if not returnIntegers else normalizers.normalizeRounding(
                v)
        return d

    # -------------------
    # Inherited Functions
    # -------------------

    def __contains__(self, pair):
        """
        Tests to see if a pair is in the Kerning.
        **pair** will be a ``tuple`` of two :ref:`type-string`\s.

        This returns a ``bool`` indicating if the **pair**
        is in the Kerning. ::

            >>> ("A", "V") in font.kerning
            True
        """
        return super(BaseKerning, self).__contains__(pair)

    def __delitem__(self, pair):
        """
        Removes **pair** from the Kerning. **pair** is a ``tuple`` of two
        :ref:`type-string`\s.::

            >>> del font.kerning[("A","V")]
        """
        super(BaseKerning, self).__delitem__(pair)

    def __getitem__(self, pair):
        """
        Returns the kerning value of the pair. **pair** is a ``tuple`` of
        two :ref:`type-string`\s.

        The returned value will be a :ref:`type-int-float`.::

            >>> font.kerning[("A", "V")]
            -15

        It is important to understand that any changes to the returned value
        will not be reflected in the Kerning object. If one wants to make a change to
        the value, one should do the following::

            >>> value = font.kerning[("A", "V")]
            >>> value += 10
            >>> font.kerning[("A", "V")] = value
        """
        return super(BaseKerning, self).__getitem__(pair)

    def __iter__(self):
        """
        Iterates through the Kerning, giving the pair for each iteration. The order that
        the Kerning will iterate though is not fixed nor is it ordered.::

            >>> for pair in font.kerning:
            >>>     print pair
            ("A", "Y")
            ("A", "V")
            ("A", "W")
        """
        return super(BaseKerning, self).__iter__()

    def __len__(self):
        """
        Returns the number of pairs in Kerning as an ``int``.::

            >>> len(font.kerning)
            5
        """
        return super(BaseKerning, self).__len__()

    def __setitem__(self, pair, value):
        """
        Sets the **pair** to the list of **value**. **pair** is the
        pair as a ``tuple`` of two :ref:`type-string`\s and **value**

        is a :ref:`type-int-float`.

            >>> font.kerning[("A", "V")] = -20
            >>> font.kerning[("A", "W")] = -10.5
        """
        super(BaseKerning, self).__setitem__(pair, value)

    def clear(self):
        """
        Removes all information from Kerning,
        resetting the Kerning to an empty dictionary. ::

            >>> font.kerning.clear()
        """
        super(BaseKerning, self).clear()

    def get(self, pair, default=None):
        """
        Returns the value for the kerning pair.
        **pair** is a ``tuple`` of two :ref:`type-string`\s, and the returned
        values will either be :ref:`type-int-float` or ``None``
        if no pair was found. ::

            >>> font.kerning[("A", "V")]
            -25

        It is important to understand that any changes to the returned value
        will not be reflected in the Kerning object. If one wants to make a change to
        the value, one should do the following::

            >>> value = font.kerning[("A", "V")]
            >>> value += 10
            >>> font.kerning[("A", "V")] = value
        """
        return super(BaseKerning, self).get(pair, default)

    def find(self, pair, default=None):
        """
        Returns the value for the kerning pair.
        **pair** is a ``tuple`` of two :ref:`type-string`\s, and the returned
        values will either be :ref:`type-int-float` or ``None``
        if no pair was found. ::

            >>> font.kerning[("A", "V")]
            -25
        """
        pair = normalizers.normalizeKerningKey(pair)
        value = self._find(pair, default)
        if value != default:
            value = normalizers.normalizeKerningValue(value)
        return value

    def _find(self, pair, default=None):
        """
        This is the environment implementation of
        :attr:`BaseKerning.find`. This must return an
        :ref:`type-int-float` or `default`.
        """
        from ufoLib.kerning import lookupKerningValue
        font = self.font
        groups = font.groups
        return lookupKerningValue(pair, self, groups, fallback=default)

    def items(self):
        """
        Returns a list of ``tuple``\s of each pair and value. Pairs are a
        ``tuple`` of two :ref:`type-string`\s and values are :ref:`type-int-float`.

        The initial list will be unordered.

            >>> font.kerning.items()
            [(("A", "V"), -30), (("A", "W"), -10)]
        """
        return super(BaseKerning, self).items()

    def keys(self):
        """
        Returns a ``list`` of all the pairs in Kerning. This list will be
        unordered.::

            >>> font.kerning.keys()
            [("A", "Y"), ("A", "V"), ("A", "W")]
        """
        return super(BaseKerning, self).keys()

    def pop(self, pair, default=None):
        """
        Removes the **pair** from the Kerning and returns the value as an ``int``.
        If no pair is found, **default** is returned. **pair** is a
        ``tuple`` of two :ref:`type-string`\s. This must return either

        **default** or a :ref:`type-int-float`.

            >>> font.kerning.pop(("A", "V"))
            -20
            >>> font.kerning.pop(("A", "W"))
            -10.5
        """
        return super(BaseKerning, self).pop(pair, default)

    def update(self, otherKerning):
        """
        Updates the Kerning based on **otherKerning**. **otherKerning** is a ``dict`` of
        kerning information. If a pair from **otherKerning** is in Kerning, the pair
        value will be replaced by the value from **otherKerning**. If a pair
        from **otherKerning** is not in the Kerning, it is added to the pairs. If Kerning
        contains a pair that is not in **otherKerning**, it is not changed.

            >>> font.kerning.update(newKerning)
        """
        super(BaseKerning, self).update(otherKerning)

    def values(self):
        """
        Returns a ``list`` of each pair's values, the values will be
        :ref:`type-int-float`\s.

        The list will be unordered.

            >>> font.kerning.items()
            [-20, -15, 5, 3.5]
        """
        return super(BaseKerning, self).values()
Esempio n. 21
0
class BaseGuideline(BaseObject, TransformationMixin, DeprecatedGuideline,
                    RemovedGuideline, PointPositionMixin, InterpolationMixin,
                    SelectionMixin):
    """
    A guideline object. This object is almost always
    created with :meth:`BaseGlyph.appendGuideline`.
    An orphan guideline can be created like this::

        >>> guideline = RGuideline()
    """

    copyAttributes = ("x", "y", "angle", "name", "color")

    def _reprContents(self):
        contents = []
        if self.name is not None:
            contents.append("'%s'" % self.name)
        if self.layer is not None:
            contents.append("('%s')" % self.layer.name)
        return contents

    # -------
    # Parents
    # -------

    def getParent(self):
        """
        Return the guideline's parent :class:`fontParts.base.BaseGlyph`.
        This is a backwards compatibility method.
        """
        glyph = self.glyph
        if glyph is not None:
            return glyph
        return self.font

    # Glyph

    _glyph = None

    glyph = dynamicProperty("glyph",
                            "The guideline's parent :class:`BaseGlyph`.")

    def _get_glyph(self):
        if self._glyph is None:
            return None
        return self._glyph()

    def _set_glyph(self, glyph):
        assert self._font is None
        assert self._glyph is None
        if glyph is not None:
            glyph = reference(glyph)
        self._glyph = glyph

    # Layer

    layer = dynamicProperty("layer",
                            "The guideline's parent :class:`BaseLayer`.")

    def _get_layer(self):
        if self._glyph is None:
            return None
        return self.glyph.layer

    # Font

    _font = None

    font = dynamicProperty("font", "The guideline's parent :class:`BaseFont`.")

    def _get_font(self):
        if self._font is not None:
            return self._font()
        elif self._glyph is not None:
            return self.glyph.font
        return None

    def _set_font(self, font):
        assert self._font is None
        assert self._glyph is None
        if font is not None:
            font = reference(font)
        self._font = font

    # --------
    # Position
    # --------

    # x

    x = dynamicProperty(
        "base_x", """
        The x coordinate of the guideline.
        It must be an :ref:`type-int-float`. ::

            >>> guideline.x
            100
            >>> guideline.x = 101
        """)

    def _get_base_x(self):
        value = self._get_x()
        if value is None:
            return 0
        value = normalizers.normalizeX(value)
        return value

    def _set_base_x(self, value):
        if value is None:
            value = 0
        else:
            value = normalizers.normalizeX(value)
        self._set_x(value)

    def _get_x(self):
        """
        This is the environment implementation of
        :attr:`BaseGuideline.x`. This must return an
        :ref:`type-int-float`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_x(self, value):
        """
        This is the environment implementation of
        :attr:`BaseGuideline.x`. **value** will be
        an :ref:`type-int-float`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # y

    y = dynamicProperty(
        "base_y", """
        The y coordinate of the guideline.
        It must be an :ref:`type-int-float`. ::

            >>> guideline.y
            100
            >>> guideline.y = 101
        """)

    def _get_base_y(self):
        value = self._get_y()
        if value is None:
            return 0
        value = normalizers.normalizeY(value)
        return value

    def _set_base_y(self, value):
        if value is None:
            value = 0
        else:
            value = normalizers.normalizeY(value)
        self._set_y(value)

    def _get_y(self):
        """
        This is the environment implementation of
        :attr:`BaseGuideline.y`. This must return an
        :ref:`type-int-float`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_y(self, value):
        """
        This is the environment implementation of
        :attr:`BaseGuideline.y`. **value** will be
        an :ref:`type-int-float`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # angle

    angle = dynamicProperty(
        "base_angle", """
        The angle of the guideline.
        It must be an :ref:`type-angle`.
        Please check how :func:`normalizers.normalizeGuidelineAngle`
        handles the angle. There is a special case, when angle is ``None``.
        If so, when x and y are not 0, the angle will be 0. If x is 0 but y
        is not, the angle will be 0. If y is 0 and x is not, the
        angle will be 90. If both x and y are 0, the angle will be 0.
        ::

            >>> guideline.angle
            45.0
            >>> guideline.angle = 90
        """)

    def _get_base_angle(self):
        value = self._get_angle()
        if value is None:
            if self._get_x() != 0 and self._get_y() != 0:
                value = 0
            elif self._get_x() != 0 and self._get_y() == 0:
                value = 90
            elif self._get_x() == 0 and self._get_y() != 0:
                value = 0
            else:
                value = 0
        value = normalizers.normalizeGuidelineAngle(value)
        return value

    def _set_base_angle(self, value):
        if value is None:
            if self._get_x() != 0 and self._get_y() != 0:
                value = 0
            elif self._get_x() != 0 and self._get_y() == 0:
                value = 90
            elif self._get_x() == 0 and self._get_y() != 0:
                value = 0
            else:
                value = 0
        value = normalizers.normalizeGuidelineAngle(value)
        self._set_angle(value)

    def _get_angle(self):
        """
        This is the environment implementation of
        :attr:`BaseGuideline.angle`. This must return an
        :ref:`type-angle`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_angle(self, value):
        """
        This is the environment implementation of
        :attr:`BaseGuideline.angle`. **value** will be
        an :ref:`type-angle`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # --------------
    # Identification
    # --------------

    # index

    index = dynamicProperty(
        "base_index", """
        The index of the guideline within the ordered
        list of the parent glyph's guidelines. This
        attribute is read only. ::

            >>> guideline.index
            0
        """)

    def _get_base_index(self):
        value = self._get_index()
        value = normalizers.normalizeIndex(value)
        return value

    def _get_index(self):
        """
        Get the guideline's index.
        This must return an ``int``.

        Subclasses may override this method.
        """
        glyph = self.glyph
        if glyph is not None:
            parent = glyph
        else:
            parent = self.font
        if parent is None:
            return None
        return parent.guidelines.index(self)

    # name

    name = dynamicProperty(
        "base_name", """
        The name of the guideline. This will be a
        :ref:`type-string` or ``None``.

            >>> guideline.name
            'my guideline'
            >>> guideline.name = None
        """)

    def _get_base_name(self):
        value = self._get_name()
        if value is not None:
            value = normalizers.normalizeGuidelineName(value)
        return value

    def _set_base_name(self, value):
        if value is not None:
            value = normalizers.normalizeGuidelineName(value)
        self._set_value(value)

    def _get_name(self):
        """
        This is the environment implementation of
        :attr:`BaseGuideline.name`. This must return a
        :ref:`type-string` or ``None``. The returned
        value will be normalized with
        :func:`normalizers.normalizeGuidelineName`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_name(self, value):
        """
        This is the environment implementation of
        :attr:`BaseGuideline.name`. **value** will be
        a :ref:`type-string` or ``None``. It will
        have been normalized with
        :func:`normalizers.normalizeGuidelineName`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # identifier

    identifier = dynamicProperty(
        "base_identifier", """
        The unique identifier for the guideline.
        This value will be an :ref:`type-identifier` or `None`.
        This attribute is read only. ::

            >>> guideline.identifier
            'ILHGJlygfds'

        To request an identifier if it does not exist use
        `guideline.getIdentifier()`
        """)

    def _get_base_identifier(self):
        value = self._get_identifier()
        value = normalizers.normalizeIdentifier(value)
        return value

    def _get_identifier(self):
        """
        This is the environment implementation of
        :attr:`BaseGuideline.identifier`. This must
        return an :ref:`type-identifier`. If
        the native guideline does not have an identifier
        assigned, one should be assigned and returned.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def getIdentifier(self):
        """
        Create a new, unique identifier for and assign it to the guideline.
        If the guideline already has an identifier, the existing one should be returned.
        """
        return self._getIdentifier()

    def _getIdentifier(self):
        """
        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _setIdentifier(self, value):
        """
        This method is used internally to force a specific
        identifier onto an object in certain situations.
        Subclasses that allow setting an identifier to a
        specific value may override this method.
        """
        pass

    # color

    color = dynamicProperty(
        "base_color", """"
        The guideline's color. This will be a
        :ref:`type-color` or ``None``. ::

            >>> guideline.color
            None
            >>> guideline.color = (1, 0, 0, 0.5)
        """)

    def _get_base_color(self):
        value = self._get_color()
        if value is not None:
            value = normalizers.normalizeColor(value)
            value = Color(value)
        return value

    def _set_base_color(self, value):
        if value is not None:
            value = normalizers.normalizeColor(value)
        self._set_color(value)

    def _get_color(self):
        """
        This is the environment implementation of
        :attr:`BaseGuideline.color`. This must return
        a :ref:`type-color` or ``None``. The
        returned value will be normalized with
        :func:`normalizers.normalizeColor`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_color(self, value):
        """
        This is the environment implementation of
        :attr:`BaseGuideline.color`. **value** will
        be a :ref:`type-color` or ``None``.
        It will have been normalized with
        :func:`normalizers.normalizeColor`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # --------------
    # Transformation
    # --------------

    def _transformBy(self, matrix, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseGuideline.transformBy`.

        **matrix** will be a :ref:`type-transformation`.
        that has been normalized with :func:`normalizers.normalizeTransformationMatrix`.

        Subclasses may override this method.
        """
        t = transform.Transform(*matrix)
        # coordinates
        x, y = t.transformPoint((self.x, self.y))
        self.x = x
        self.y = y
        # angle
        angle = math.radians(self.angle)
        dx = math.cos(angle)
        dy = math.sin(angle)
        tdx, tdy = t.transformPoint((dx, dy))
        ta = math.atan2(tdy, tdx)
        self.angle = math.degrees(ta)

    # -------------
    # Interpolation
    # -------------

    compatibilityReporterClass = GuidelineCompatibilityReporter

    def isCompatible(self, other):
        """
        Evaluate interpolation compatibility with **other**. ::

            >>> compatible, report = self.isCompatible(otherGuideline)
            >>> compatible
            True
            >>> compatible
            [Warning] Guideline: "xheight" + "cap_height"
            [Warning] Guideline: "xheight" has name xheight | "cap_height" has name cap_height

        This will return a ``bool`` indicating if the guideline is
        compatible for interpolation with **other** and a
        :ref:`type-string` of compatibility notes.
        """
        return super(BaseGuideline, self).isCompatible(other, BaseGuideline)

    def _isCompatible(self, other, reporter):
        """
        This is the environment implementation of
        :meth:`BaseGuideline.isCompatible`.

        Subclasses may override this method.
        """
        guideline1 = self
        guideline2 = other
        # guideline names
        if guideline1.name != guideline2.name:
            reporter.nameDifference = True
            reporter.warning = True

    # -------------
    # Normalization
    # -------------

    def round(self):
        """
        Round the guideline's coordinate.

            >>> guideline.round()

        This applies to the following:

        * x
        * y

        It does not apply to

        * angle
        """
        self._round()

    def _round(self, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseGuideline.round`.

        Subclasses may override this method.
        """
        self.x = normalizers.normalizeRounding(self.x)
        self.y = normalizers.normalizeRounding(self.y)
Esempio n. 22
0
class BaseFont(_BaseGlyphVendor, InterpolationMixin, DeprecatedFont,
               RemovedFont):
    """
    A font object. This object is almost always
    created with one of the font functions in
    :ref:`fontparts-world`.
    """
    def __init__(self, pathOrObject=None, showInterface=True):
        """
        When constructing a font, the object can be created
        in a new file, from an existing file or from a native
        object. This is defined with the **pathOrObjectArgument**.
        If **pathOrObject** is a string, the string must represent
        an existing file. If **pathOrObject** is an instance of the
        environment's unwrapped native font object, wrap it with
        FontParts. If **pathOrObject** is None, create a new,
        empty font. If **showInterface** is ``False``, the font
        should be created without graphical interface. The default
        for **showInterface** is ``True``.
        """
        super(BaseFont, self).__init__(pathOrObject=pathOrObject,
                                       showInterface=showInterface)

    def _reprContents(self):
        contents = [
            "'%s %s'" % (self.info.familyName, self.info.styleName),
        ]
        if self.path is not None:
            contents.append("path=%r" % self.path)
        return contents

    def __hash__(self):
        """
        Allow font object to be used as a key
        in a dictionary.

        Subclasses may override this method.
        """
        return id(self.naked())

    # ----
    # Copy
    # ----

    copyAttributes = ("info", "groups", "kerning", "features", "lib",
                      "layerOrder", "defaultLayerName", "glyphOrder")

    def copy(self):
        """
        Copy the font into a new font. ::

            >>> copiedFont = font.copy()

        This will copy:

        * info
        * groups
        * kerning
        * features
        * lib
        * layers
        * layerOrder
        * defaultLayerName
        * glyphOrder
        * guidelines
        """
        return super(BaseFont, self).copy()

    def copyData(self, source):
        """
        Copy data from **source** into this font.
        Refer to :meth:`BaseFont.copy` for a list
        of values that will be copied.
        """
        for layerName in source.layerOrder:
            if layerName in self.layerOrder:
                layer = self.getLayer(layerName)
            else:
                layer = self.newLayer(layerName)
            layer.copyData(source.getLayer(layerName))
        for sourceGuideline in self.guidelines:
            selfGuideline = self.appendGuideline((0, 0), 0)
            selfGuideline.copyData(sourceGuideline)
        super(BaseFont, self).copyData(source)

    # ---------------
    # File Operations
    # ---------------

    # Initialize

    def _init(self, pathOrObject=None, showInterface=True, **kwargs):
        """
        Initialize this object. This should wrap a native font
        object based on the values for **pathOrObject**:

        +--------------------+---------------------------------------------------+
        | None               | Create a new font.                                |
        +--------------------+---------------------------------------------------+
        | string             | Open the font file located at the given location. |
        +--------------------+---------------------------------------------------+
        | native font object | Wrap the given object.                            |
        +--------------------+---------------------------------------------------+

        If **showInterface** is ``False``, the font should be
        created without graphical interface.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # path

    path = dynamicProperty(
        "base_path", """
        The path to the file this object represents. ::

            >>> print font.path
            "/path/to/my/font.ufo"
        """)

    def _get_base_path(self):
        path = self._get_path()
        if path is not None:
            path = normalizers.normalizeFilePath(path)
        return path

    def _get_path(self, **kwargs):
        """
        This is the environment implementation of
        :attr:`BaseFont.path`.

        This must return a :ref:`type-string` defining the
        location of the file or ``None`` indicating that the
        font does not have a file representation. If the
        returned value is not ``None`` it will be normalized
        with :func:`normalizers.normalizeFilePath`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # save

    def save(self, path=None, showProgress=False, formatVersion=None):
        """
        Save the font to **path**.

            >>> font.save()
            >>> font.save("/path/to/my/font-2.ufo")

        If **path** is None, use the font's original location.
        The file type must be inferred from the file extension
        of the given path. If no file extension is given, the
        environment may fall back to the format of its choice.
        **showProgress** indicates if a progress indicator should
        be displayed during the operation. Environments may or may
        not implement this behavior. **formatVersion** indicates
        the format version that should be used for writing the given
        file type. For example, if 2 is given for formatVersion
        and the file type being written if UFO, the file is to
        be written in UFO 2 format. This value is not limited
        to UFO format versions. If no format version is given,
        the original format version of the file should be preserved.
        If there is no original format version it is implied that
        the format version is the latest version for the file
        type as supported by the environment.

        .. note::

           Environments may define their own rules governing when
           a file should be saved into its original location and
           when it should not. For example, a font opened from a
           compiled OpenType font may not be written back into
           the original OpenType font.
        """
        if path is None and self.path is None:
            raise IOError(("The font cannot be saved because no file "
                           "location has been given."))
        if path is not None:
            path = normalizers.normalizeFilePath(path)
        showProgress = bool(showProgress)
        if formatVersion is not None:
            formatVersion = normalizers.normalizeFileFormatVersion(
                formatVersion)
        self._save(path=path,
                   showProgress=showProgress,
                   formatVersion=formatVersion)

    def _save(self,
              path=None,
              showProgress=False,
              formatVersion=None,
              **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseFont.save`. **path** will be a
        :ref:`type-string` or ``None``. If **path**
        is not ``None``, the value will have been
        normalized with :func:`normalizers.normalizeFilePath`.
        **showProgress** will be a ``bool`` indicating if
        the environment should display a progress bar
        during the operation. Environments are not *required*
        to display a progress bar even if **showProgess**
        is ``True``. **formatVersion** will be :ref:`type-int-float`
        or ``None`` indicating the file format version
        to write the data into. It will have been normalized
        with :func:`normalizers.normalizeFileFormatVersion`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # close

    def close(self, save=False):
        """
        Close the font.

            >>> font.close()

        **save** is a boolean indicating if the font
        should be saved prior to closing. If **save**
        is ``True``, the :meth:`BaseFont.save` method
        will be called. The default is ``False``.
        """
        if save:
            self.save()
        self._close()

    def _close(self, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseFont.close`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # generate

    def generateFormatToExtension(self, format, fallbackFormat):
        """
        +--------------+--------------------------------------------------------------------+
        | mactype1     | Mac Type 1 font (generates suitcase  and LWFN file)                |
        +--------------+--------------------------------------------------------------------+
        | macttf       | Mac TrueType font (generates suitcase)                             |
        +--------------+--------------------------------------------------------------------+
        | macttdfont   | Mac TrueType font (generates suitcase with resources in data fork) |
        +--------------+--------------------------------------------------------------------+
        | otfcff       | PS OpenType (CFF-based) font (OTF)                                 |
        +--------------+--------------------------------------------------------------------+
        | otfttf       | PC TrueType/TT OpenType font (TTF)                                 |
        +--------------+--------------------------------------------------------------------+
        | pctype1      | PC Type 1 font (binary/PFB)                                        |
        +--------------+--------------------------------------------------------------------+
        | pcmm         | PC MultipleMaster font (PFB)                                       |
        +--------------+--------------------------------------------------------------------+
        | pctype1ascii | PC Type 1 font (ASCII/PFA)                                         |
        +--------------+--------------------------------------------------------------------+
        | pcmmascii    | PC MultipleMaster font (ASCII/PFA)                                 |
        +--------------+--------------------------------------------------------------------+
        | ufo1         | UFO format version 1                                               |
        +--------------+--------------------------------------------------------------------+
        | ufo2         | UFO format version 2                                               |
        +--------------+--------------------------------------------------------------------+
        | ufo3         | UFO format version 3                                               |
        +--------------+--------------------------------------------------------------------+
        | unixascii    | UNIX ASCII font (ASCII/PFA)                                        |
        +--------------+--------------------------------------------------------------------+
        """
        formatToExtension = dict(
            # mactype1=None,
            macttf=".ttf",
            macttdfont=".dfont",
            otfcff=".otf",
            otfttf=".ttf",
            # pctype1=None,
            # pcmm=None,
            # pctype1ascii=None,
            # pcmmascii=None,
            ufo1=".ufo",
            ufo2=".ufo",
            ufo3=".ufo",
            unixascii=".pfa",
        )
        return formatToExtension.get(format, fallbackFormat)

    def generate(self, format, path=None):
        """
        Generate the font to another format.

            >>> font.generate("otfcff")
            >>> font.generate("otfcff", "/path/to/my/font.otf")

        **format** defines the file format to output. These are the
        standard format identifiers:

        %s

        Environments are not required to support all of these
        and environments may define their own format types.
        **path** defines the location where the new file should
        be created. If a file already exists at that location,
        it will be overwritten by the new file. If **path** defines
        a directory, the file will be output as the current
        file name, with the appropriate suffix for the format,
        into the given directory. If no **path** is given, the
        file will be output into the same directory as the source
        font with the file named with the current file name,
        with the appropriate suffix for the format.
        """

        if format is None:
            raise ValueError("The format must be defined when generating.")
        elif not isinstance(format, basestring):
            raise TypeError("The format must be defined as a string.")
        ext = self.generateFormatToExtension(format, "." + format)
        if path is None and self.path is None:
            raise IOError(("The file cannot be generated because an "
                           "output path was not defined."))
        elif path is None:
            path = os.path.splitext(self.path)[0]
            path += ext
        elif os.path.isdir(path):
            if self.path is None:
                raise IOError(("The file cannot be generated because "
                               "the file does not have a path."))
            fileName = os.path.basename(self.path)
            fileName += ext
            path = os.path.join(path, fileName)
        path = normalizers.normalizeFilePath(path)
        return self._generate(format=format, path=path)

    generate.__doc__ %= generateFormatToExtension.__doc__

    def _generate(self, format, path, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseFont.generate`. **format** will be a
        :ref:`type-string` defining the output format.
        Refer to the :meth:`BaseFont.generate` documentation
        for the standard format identifiers. If the value
        given for **format** is not supported by the environment,
        the environment must raise :exc:`FontPartsError`.
        **path** will be a :ref:`type-string` defining the
        location where the file should be created. It
        will have been normalized with :func:`normalizers.normalizeFilePath`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # -----------
    # Sub-Objects
    # -----------

    # info

    info = dynamicProperty(
        "base_info", """
        The font's :class:`BaseInfo` object.

            >>> font.info.familyName
            "My Family"
        """)

    def _get_base_info(self):
        info = self._get_info()
        info.font = self
        return info

    def _get_info(self):
        """
        This is the environment implementation of
        :attr:`BaseFont.info`. This must return an
        instance of a :class:`BaseInfo` subclass.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # groups

    groups = dynamicProperty(
        "base_groups", """
        The font's :class:`BaseGroups` object.

            >>> font.groups["myGroup"]
            ["A", "B", "C"]
        """)

    def _get_base_groups(self):
        groups = self._get_groups()
        groups.font = self
        return groups

    def _get_groups(self):
        """
        This is the environment implementation of
        :attr:`BaseFont.groups`. This must return
        an instance of a :class:`BaseGroups` subclass.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # kerning

    kerning = dynamicProperty(
        "base_kerning", """
        The font's :class:`BaseKerning` object.

            >>> font.kerning["A", "B"]
            -100
        """)

    def _get_base_kerning(self):
        kerning = self._get_kerning()
        kerning.font = self
        return kerning

    def _get_kerning(self):
        """
        This is the environment implementation of
        :attr:`BaseFont.kerning`. This must return
        an instance of a :class:`BaseKerning` subclass.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # features

    features = dynamicProperty(
        "base_features", """
        The font's :class:`BaseFeatures` object.

            >>> font.features.text
            "include(features/substitutions.fea);"
        """)

    def _get_base_features(self):
        features = self._get_features()
        features.font = self
        return features

    def _get_features(self):
        """
        This is the environment implementation of
        :attr:`BaseFont.features`. This must return
        an instance of a :class:`BaseFeatures` subclass.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # lib

    lib = dynamicProperty(
        "base_lib", """
        The font's :class:`BaseLib` object.

            >>> font.lib["org.robofab.hello"]
            "world"
        """)

    def _get_base_lib(self):
        lib = self._get_lib()
        lib.font = self
        return lib

    def _get_lib(self):
        """
        This is the environment implementation of
        :attr:`BaseFont.lib`. This must return an
        instance of a :class:`BaseLib` subclass.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # -----------------
    # Layer Interaction
    # -----------------

    layers = dynamicProperty(
        "base_layers", """
        The font's :class:`BaseLayer` objects.

            >>> for layer in font.layers:
            ...     layer.name
            "My Layer 1"
            "My Layer 2"
        """)

    def _get_base_layers(self):
        layers = self._get_layers()
        for layer in layers:
            self._setFontInLayer(layer)
        return tuple(layers)

    def _get_layers(self, **kwargs):
        """
        This is the environment implementation of
        :attr:`BaseFont.layers`. This must return an
        :ref:`type-immutable-list` containing
        instances of :class:`BaseLayer` subclasses.
        The items in the list should be in the order
        defined by :attr:`BaseFont.layerOrder`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # order

    layerOrder = dynamicProperty(
        "base_layerOrder", """
        A list of layer names indicating order of the layers in the font.

            >>> font.layerOrder = ["My Layer 2", "My Layer 1"]
            >>> font.layerOrder
            ["My Layer 2", "My Layer 1"]
        """)

    def _get_base_layerOrder(self):
        value = self._get_layerOrder()
        value = normalizers.normalizeLayerOrder(value, self)
        return list(value)

    def _set_base_layerOrder(self, value):
        value = normalizers.normalizeLayerOrder(value, self)
        self._set_layerOrder(value)

    def _get_layerOrder(self, **kwargs):
        """
        This is the environment implementation of
        :attr:`BaseFont.layerOrder`. This must return an
        :ref:`type-immutable-list` defining the order of
        the layers in the font. The contents of the list
        must be layer names as :ref:`type-string`. The
        list will be normalized with :func:`normalizers.normalizeLayerOrder`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_layerOrder(self, value, **kwargs):
        """
        This is the environment implementation of
        :attr:`BaseFont.layerOrder`. **value** will
        be a **list** of :ref:`type-string` representing
        layer names. The list will have been normalized
        with :func:`normalizers.normalizeLayerOrder`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # default layer

    def _setFontInLayer(self, layer):
        if layer.font is None:
            layer.font = self

    defaultLayerName = dynamicProperty(
        "base_defaultLayerName", """
        The name of the font's default layer.

            >>> font.defaultLayerName = "My Layer 2"
            >>> font.defaultLayerName
            "My Layer 2"
        """)

    def _get_base_defaultLayerName(self):
        value = self._get_defaultLayerName()
        value = normalizers.normalizeDefaultLayerName(value, self)
        return value

    def _set_base_defaultLayerName(self, value):
        value = normalizers.normalizeDefaultLayerName(value, self)
        self._set_defaultLayerName(value)

    def _get_defaultLayerName(self):
        """
        This is the environment implementation of
        :attr:`BaseFont.defaultLayerName`. Return the
        name of the default layer as a :ref:`type-string`.
        The name will be normalized with
        :func:`normalizers.normalizeDefaultLayerName`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_defaultLayerName(self, value, **kwargs):
        """
        This is the environment implementation of
        :attr:`BaseFont.defaultLayerName`. **value**
        will be a :ref:`type-string`. It will have
        been normalized with :func:`normalizers.normalizeDefaultLayerName`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    defaultLayer = dynamicProperty(
        "base_defaultLayer", """
        The font's default layer.

            >>> layer = font.defaultLayer
            >>> font.defaultLayer = otherLayer
        """)

    def _get_defaultLayer(self):
        layer = self._get_base_defaultLayer()
        layer = normalizers.normalizeLayer(layer)
        return layer

    def _set_defaultLayer(self):
        layer = normalizers.normalizeLayer(layer)
        self._set_base_defaultLayer(layer)

    def _get_base_defaultLayer(self):
        """
        This is the environment implementation of
        :attr:`BaseFont.defaultLayer`. Return the
        default layer as a :class:`BaseLayer` object.
        The layer will be normalized with
        :func:`normalizers.normalizeLayer`.

        Subclasses must override this method.
        """
        name = self.defaultLayerName
        layer = self.getLayer(name)
        return layer

    def _set_base_defaultLayer(self, value):
        """
        This is the environment implementation of
        :attr:`BaseFont.defaultLayer`. **value**
        will be a :class:`BaseLayer`. It will have
        been normalized with :func:`normalizers.normalizeLayer`.

        Subclasses must override this method.
        """
        self.defaultLayerName = value.name

    # get

    def getLayer(self, name):
        """
        Get the :class:`BaseLayer` with **name**.

            >>> layer = font.getLayer("My Layer 2")
        """
        name = normalizers.normalizeLayerName(name)
        if name not in self.layerOrder:
            raise ValueError("No layer with the name '%s' exists." % name)
        layer = self._getLayer(name)
        self._setFontInLayer(layer)
        return layer

    def _getLayer(self, name, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseFont.getLayer`. **name** will
        be a :ref:`type-string`. It will have been
        normalized with :func:`normalizers.normalizeLayerName`
        and it will have been verified as an existing layer.
        This must return an instance of :class:`BaseLayer`.

        Subclasses may override this method.
        """
        for layer in self.layers:
            if layer.name == name:
                return layer

    # new

    def newLayer(self, name, color=None):
        """
        Make a new layer with **name** and **color**.
        **name** must be a :ref:`type-string` and
        **color** must be a :ref:`type-color` or ``None``.

            >>> layer = font.newLayer("My Layer 3")

        The will return the newly created
        :class:`BaseLayer`.
        """
        name = normalizers.normalizeLayerName(name)
        if name in self.layerOrder:
            layer = self.getLayer(name)
            if color is not None:
                layer.color = color
            return layer
        if color is not None:
            color = normalizers.normalizeColor(color)
        layer = self._newLayer(name=name, color=color)
        self._setFontInLayer(layer)
        return layer

    def _newLayer(self, name, color, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseFont.newLayer`. **name** will be
        a :ref:`type-string` representing a valid
        layer name. The value will have been normalized
        with :func:`normalizers.normalizeLayerName` and
        **name** will not be the same as the name of
        an existing layer. **color** will be a
        :ref:`type-color` or ``None``. If the value
        is not ``None`` the value will have been
        normalized with :func:`normalizers.normalizeColor`.
        This must return an instance of a :class:`BaseLayer`
        subclass that represents the new layer.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # remove

    def removeLayer(self, name):
        """
        Remove the layer with **name** from the font.

            >>> font.removeLayer("My Layer 3")
        """
        name = normalizers.normalizeLayerName(name)
        if name not in self.layerOrder:
            raise ValueError("No layer with the name '%s' exists." % name)
        self._removeLayer(name)

    def _removeLayer(self, name, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseFont.removeLayer`. **name** will
        be a :ref:`type-string` defining the name
        of an existing layer. The value will have
        been normalized with :func:`normalizers.normalizeLayerName`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # -----------------
    # Glyph Interaction
    # -----------------

    # base implementation overrides

    def _getItem(self, name, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseFont.__getitem__`. **name** will
        be a :ref:`type-string` defining an existing
        glyph in the default layer. The value will
        have been normalized with :func:`normalizers.normalizeGlyphName`.

        Subclasses may override this method.
        """
        layer = self.defaultLayer
        return layer[name]

    def _keys(self, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseFont.keys`. This must return an
        :ref:`type-immutable-list` of all glyph names
        in the default layer.

        Subclasses may override this method.
        """
        layer = self.defaultLayer
        return layer.keys()

    def _newGlyph(self, name, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseFont.newGlyph`. **name** will be
        a :ref:`type-string` representing a valid
        glyph name. The value will have been tested
        to make sure that an existing glyph in the
        default layer does not have an identical name.
        The value will have been normalized with
        :func:`normalizers.normalizeGlyphName`. This
        must return an instance of :class:`BaseGlyph`
        representing the new glyph.

        Subclasses may override this method.
        """
        layer = self.defaultLayer
        # clear is False here because the base newFont
        # that has called this method will have already
        # handled the clearing as specified by the caller.
        return layer.newGlyph(name, clear=False)

    def _removeGlyph(self, name, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseFont.removeGlyph`. **name** will
        be a :ref:`type-string` representing an
        existing glyph in the default layer. The
        value will have been normalized with
        :func:`normalizers.normalizeGlyphName`.

        Subclasses may override this method.
        """
        layer = self.defaultLayer
        layer.removeGlyph(name)

    # order

    glyphOrder = dynamicProperty(
        "base_glyphOrder", """
        The preferred order of the glyphs in the font.

            >>> font.glyphOrder
            ["C", "B", "A"]
            >>> font.glyphOrder = ["A", "B", "C"]
        """)

    def _get_base_glyphOrder(self):
        value = self._get_glyphOrder()
        value = normalizers.normalizeGlyphOrder(value)
        return value

    def _set_base_glyphOrder(self, value):
        value = normalizers.normalizeGlyphOrder(value)
        self._set_glyphOrder(value)

    def _get_glyphOrder(self):
        """
        This is the environment implementation of
        :attr:`BaseFont.glyphOrder`. This must return
        an :ref:`type-immutable-list` containing glyph
        names representing the glyph order in the font.
        The value will be normalized with
        :func:`normalizers.normalizeGlyphOrder`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_glyphOrder(self, value):
        """
        This is the environment implementation of
        :attr:`BaseFont.glyphOrder`. **value** will
        be a list of :ref:`type-string`. It will
        have been normalized with
        :func:`normalizers.normalizeGlyphOrder`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # -----------------
    # Global Operations
    # -----------------

    def round(self):
        """
        Round all approriate data to integers.

            >>> font.round()

        This is the equivalent of calling the round method on:

        * info
        * kerning
        * the default layer
        * font-level guidelines

        This applies only to the default layer.
        """
        self._round()

    def _round(self):
        """
        This is the environment implementation of
        :meth:`BaseFont.round`.

        Subclasses may override this method.
        """
        layer = self.defaultLayer
        layer.round()
        self.info.round()
        self.kerning.round()
        for guideline in self.guidelines():
            guideline.round()

    def autoUnicodes(self):
        """
        Use heuristics to set Unicode values in all glyphs.

            >>> font.autoUnicodes()

        Environments will define their own heuristics for
        automatically determining values.

        This applies only to the default layer.
        """
        self._autoUnicodes()

    def _autoUnicodes(self):
        """
        This is the environment implementation of
        :meth:`BaseFont.autoUnicodes`.

        Subclasses may override this method.
        """
        layer = self.defaultLayer
        layer.autoUnicodes()

    # ----------
    # Guidelines
    # ----------

    def _setFontInGuideline(self, guideline):
        if guideline.font is None:
            guideline.font = self

    guidelines = dynamicProperty(
        "guidelines", """
        An :ref:`type-immutable-list` of font-level :class:`BaseGuideline` objects.

            >>> for guideline in font.guidelines:
            ...     guideline.angle
            0
            45
            90
        """)

    def _get_guidelines(self):
        """
        This is the environment implementation of
        :attr:`BaseFont.guidelines`. This must
        return an :ref:`type-immutable-list` of
        :class:`BaseGuideline` objects.

        Subclasses may override this method.
        """
        return tuple([
            self._getitem__guidelines(i)
            for i in range(self._len__guidelines())
        ])

    def _len__guidelines(self):
        return self._lenGuidelines()

    def _lenGuidelines(self, **kwargs):
        """
        This must return an integer indicating
        the number of font-level guidelines
        in the font.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _getitem__guidelines(self, index):
        index = normalizers.normalizeGuidelineIndex(index)
        if index >= self._len__guidelines():
            raise ValueError("No guideline located at index %d." % index)
        guideline = self._getGuideline(index)
        self._setFontInGuideline(guideline)
        return guideline

    def _getGuideline(self, index, **kwargs):
        """
        This must return a :class:`BaseGuideline` object.
        **index** will be a valid **index**.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _getGuidelineIndex(self, guideline):
        for i, other in enumerate(self.guidelines):
            if guideline == other:
                return i
        raise FontPartsError("The guideline could not be found.")

    def appendGuideline(self, position, angle, name=None, color=None):
        """
        Append a new guideline to the font.

            >>> guideline = font.appendGuideline((50, 0), 90)
            >>> guideline = font.appendGuideline((0, 540), 0, name="overshoot",
            >>> color=(0, 0, 0, 0.2))

        **position** must be a :ref:`type-coordinate`
        indicating the position of the guideline.
        **angle** indicates the :ref:`type-angle` of
        the guideline. **name** indicates the name
        for the guideline. This must be a :ref:`type-string`
        or ``None``. **color** indicates the color for
        the guideline. This must be a :ref:`type-color`
        or ``None``. This will return the newly created
        :class:`BaseGuidline` object.
        """
        position = normalizers.normalizeCoordinateTuple(position)
        angle = normalizers.normalizeGuidelineAngle(angle)
        if name is not None:
            name = normalizers.normalizeGuidelineName(name)
        if color is not None:
            color = normalizers.normalizeColor(color)
        return self._appendGuideline(position, angle, name=name, color=color)

    def _appendGuideline(self,
                         position,
                         angle,
                         name=None,
                         color=None,
                         **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseFont.appendGuideline`. **position**
        will be a valid :ref:`type-coordinate`. **angle**
        will be a valid angle. **name** will be a valid
        :ref:`type-string` or ``None``. **color** will
        be a valid :ref:`type-color` or ``None``.
        This must return the newly created
        :class:`BaseGuideline` object.

        Subclasses may override this method.
        """
        self.raiseNotImplementedError()

    def removeGuideline(self, guideline):
        """
        Remove **guideline** from the font.

            >>> font.removeGuideline(guideline)
            >>> font.removeGuideline(2)

        **guideline** can be a guideline object or
        an integer representing the guideline index.
        """
        if isinstance(guideline, int):
            index = guideline
        else:
            index = self._getGuidelineIndex(guideline)
        index = normalizers.normalizeGuidelineIndex(index)
        if index >= self._len__guidelines():
            raise ValueError("No guideline located at index %d." % index)
        self._removeGuideline(index)

    def _removeGuideline(self, index, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseFont.removeGuideline`. **index**
        will be a valid index.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def clearGuidelines(self):
        """
        Clear all guidelines.

            >>> font.clearGuidelines()
        """
        self._clearGuidelines()

    def _clearGuidelines(self):
        """
        This is the environment implementation of
        :meth:`BaseFont.clearGuidelines`.

        Subclasses may override this method.
        """
        for _ in range(self._len__guidelines()):
            self.removeGuideline(-1)

    # -------------
    # Interpolation
    # -------------

    def interpolate(self,
                    factor,
                    minFont,
                    maxFont,
                    round=True,
                    suppressError=True):
        """
        Interpolate all possible data in the font.

            >>> font.interpolate(0.5, otherFont1, otherFont2)
            >>> font.interpolate((0.5, 2.0), otherFont1, otherFont2, round=False)

        The interpolation occurs on a 0 to 1.0 range where **minFont**
        is located at 0 and **maxFont** is located at 1.0. **factor**
        is the interpolation value. It may be less than 0 and greater
        than 1.0. It may be a :ref:`type-int-float` or a tuple of
        two :ref:`type-int-float`. If it is a tuple, the first
        number indicates the x factor and the second number indicates
        the y factor. **round** indicates if the result should be
        rounded to integers. **suppressError** indicates if incompatible
        data should be ignored or if an error should be raised when
        such incompatibilities are found.
        """
        factor = normalizers.normalizeInterpolationFactor(factor)
        if not isinstance(minFont, BaseFont):
            raise TypeError(
                ("Interpolation to an instance of %r can not be "
                 "performed from an instance of %r.") %
                (self.__class__.__name__, minFont.__class__.__name__))
        if not isinstance(maxFont, BaseFont):
            raise TypeError(
                ("Interpolation to an instance of %r can not be "
                 "performed from an instance of %r.") %
                (self.__class__.__name__, maxFont.__class__.__name__))
        round = normalizers.normalizeBoolean(round)
        suppressError = normalizers.normalizeBoolean(suppressError)
        self._interpolate(factor,
                          minFont,
                          maxFont,
                          round=round,
                          suppressError=suppressError)

    def _interpolate(self,
                     factor,
                     minFont,
                     maxFont,
                     round=True,
                     suppressError=True):
        """
        This is the environment implementation of
        :meth:`BaseFont.interpolate`.

        Subclasses may override this method.
        """
        # layers
        for layerName in self.layerOrder:
            self.removeLayer(layerName)
        for layerName in minFont.layerOrder:
            if layerName not in maxFont.layerOrder:
                continue
            minLayer = minFont.getLayer(layerName)
            maxLayer = maxFont.getLayer(layerName)
            dstLayer = self.newLayer(layerName)
            dstLayer.interpolate(factor,
                                 minLayer,
                                 maxLayer,
                                 round=round,
                                 suppressError=suppressError)
        # kerning and groups
        self.kerning.interpolate(factor,
                                 minFont.kerning,
                                 maxFont.kerning,
                                 round=round,
                                 suppressError=suppressError)
        # info
        self.info.interpolate(factor,
                              minFont.info,
                              maxFont.info,
                              round=round,
                              suppressError=suppressError)

    compatibilityReporterClass = FontCompatibilityReporter

    def isCompatible(self, other):
        """
        Evaluate interpolation compatibility with **other**.

            >>> compatible, report = self.isCompatible(otherFont)
            >>> compatible
            False
            >>> report
            [Fatal] Glyph: "test1" + "test2"
            [Fatal] Glyph: "test1" contains 1 contours | "test2" contains 2 contours

        This will return a ``bool`` indicating if the font is
        compatible for interpolation with **other** and a
        :ref:`type-string` of compatibility notes.
        """
        return super(BaseFont, self).isCompatible(other, BaseFont)

    def _isCompatible(self, other, reporter):
        """
        This is the environment implementation of
        :meth:`BaseFont.isCompatible`.

        Subclasses may override this method.
        """
        font1 = self
        font2 = other

        # incompatible guidelines
        guidelines1 = set(font1.guidelines)
        guidelines2 = set(font2.guidelines)
        if len(guidelines1) != len(guidelines2):
            reporter.warning = True
            reporter.guidelineCountDifference = True
        if len(guidelines1.difference(guidelines2)) != 0:
            reporter.warning = True
            reporter.guidelinesMissingFromFont2 = list(
                guidelines1.difference(guidelines2))
        if len(guidelines2.difference(guidelines1)) != 0:
            reporter.warning = True
            reporter.guidelinesMissingInFont1 = list(
                guidelines2.difference(guidelines1))
        # incompatible layers
        layers1 = set(font1.layerOrder)
        layers2 = set(font2.layerOrder)
        if len(layers1) != len(layers2):
            reporter.warning = True
            reporter.layerCountDifference = True
        if len(layers1.difference(layers2)) != 0:
            reporter.warning = True
            reporter.layersMissingFromFont2 = list(layers1.difference(layers2))
        if len(layers2.difference(layers1)) != 0:
            reporter.warning = True
            reporter.layersMissingInFont1 = list(layers2.difference(layers1))
        # test layers
        for layerName in sorted(layers1.intersection(layers2)):
            layer1 = font1.getLayer(layerName)
            layer2 = font2.getLayer(layerName)
            layerCompatibility = layer1.isCompatible(layer2)[1]
            if layerCompatibility.fatal or layerCompatibility.warning:
                if layerCompatibility.fatal:
                    reporter.fatal = True
                if layerCompatibility.warning:
                    reporter.warning = True
                reporter.layers.append(layerCompatibility)

    # -------
    # mapping
    # -------

    def getReverseComponentMapping(self):
        """
        Create a dictionary of unicode -> [glyphname, ...] mappings.
        All glyphs are loaded. Note that one glyph can have multiple unicode values,
        and a unicode value can have multiple glyphs pointing to it.
        """
        return self._getReverseComponentMapping()

    def _getReverseComponentMapping(self):
        """
        This is the environment implementation of
        :meth:`BaseFont.getReverseComponentMapping`.

        Subclasses may override this method.
        """
        layer = self.defaultLayer
        return layer.getReverseComponentMapping()

    def getCharacterMapping(self):
        """
        Get a reversed map of component references in the font.
        {
        'A' : ['Aacute', 'Aring']
        'acute' : ['Aacute']
        'ring' : ['Aring']
        etc.
        }
        """
        return self._getCharacterMapping()

    def _getCharacterMapping(self):
        """
        This is the environment implementation of
        :meth:`BaseFont.getCharacterMapping`.

        Subclasses may override this method.
        """
        layer = self.defaultLayer
        return layer.getCharacterMapping()

    # ---------
    # Selection
    # ---------

    # layers

    selectedLayers = dynamicProperty(
        "base_selectedLayers", """
        A list of layers selected in the layer.

        Getting selected layer objects:

            >>> for layer in layer.selectedLayers:
            ...     layer.color = (1, 0, 0, 0.5)

        Setting selected layer objects:

            >>> layer.selectedLayers = someLayers
        """)

    def _get_base_selectedLayers(self):
        selected = tuple([
            normalizers.normalizeLayer(layer)
            for layer in self._get_selectedLayers()
        ])
        return selected

    def _get_selectedLayers(self):
        """
        Subclasses may override this method.
        """
        return self._getSelectedSubObjects(self.layers)

    def _set_base_selectedLayers(self, value):
        normalized = [normalizers.normalizeLayer(layer) for layer in value]
        self._set_selectedLayers(normalized)

    def _set_selectedLayers(self, value):
        """
        Subclasses may override this method.
        """
        return self._setSelectedSubObjects(self.layers, value)

    selectedLayerNames = dynamicProperty(
        "base_selectedLayerNames", """
        A list of names of layers selected in the layer.

        Getting selected layer names:

            >>> for name in layer.selectedLayerNames:
            ...     print(name)

        Setting selected layer names:

            >>> layer.selectedLayerNames = ["A", "B", "C"]
        """)

    def _get_base_selectedLayerNames(self):
        selected = tuple([
            normalizers.normalizeLayerName(name)
            for name in self._get_selectedLayerNames()
        ])
        return selected

    def _get_selectedLayerNames(self):
        """
        Subclasses may override this method.
        """
        selected = [layer.name for layer in self.selectedLayers]
        return selected

    def _set_base_selectedLayerNames(self, value):
        normalized = [normalizers.normalizeLayerName(name) for name in value]
        self._set_selectedLayerNames(normalized)

    def _set_selectedLayerNames(self, value):
        """
        Subclasses may override this method.
        """
        select = [self.layers(name) for name in value]
        self.selectedLayers = select

    # guidelines

    selectedGuidelines = dynamicProperty(
        "base_selectedGuidelines", """
        A list of guidelines selected in the font.

        Getting selected guideline objects:

            >>> for guideline in font.selectedGuidelines:
            ...     guideline.color = (1, 0, 0, 0.5)

        Setting selected guideline objects:

            >>> font.selectedGuidelines = someGuidelines

        Setting also supports guideline indexes:

            >>> font.selectedGuidelines = [0, 2]
        """)

    def _get_base_selectedGuidelines(self):
        selected = tuple([
            normalizers.normalizeGuideline(guideline)
            for guideline in self._get_selectedGuidelines()
        ])
        return selected

    def _get_selectedGuidelines(self):
        """
        Subclasses may override this method.
        """
        return self._getSelectedSubObjects(self.guidelines)

    def _set_base_selectedGuidelines(self, value):
        normalized = []
        for i in value:
            if isinstance(i, int):
                i = normalizers.normalizeGuidelineIndex(i)
            else:
                i = normalizers.normalizeGuideline(i)
            normalized.append(i)
        self._set_selectedGuidelines(normalized)

    def _set_selectedGuidelines(self, value):
        """
        Subclasses may override this method.
        """
        return self._setSelectedSubObjects(self.guidelines, value)
Esempio n. 23
0
class BaseSegment(
                  BaseObject,
                  TransformationMixin,
                  InterpolationMixin,
                  SelectionMixin,
                  DeprecatedSegment,
                  RemovedSegment
                  ):

    def _setPoints(self, points):
        assert not hasattr(self, "_points")
        self._points = points

    def _reprContents(self):
        contents = [
            "%s" % self.type,
        ]
        if self.index is not None:
            contents.append("index='%r'" % self.index)
        return contents

    # -------
    # Parents
    # -------

    # Contour

    _contour = None

    contour = dynamicProperty("contour", "The segment's parent contour.")

    def _get_contour(self):
        if self._contour is None:
            return None
        return self._contour()

    def _set_contour(self, contour):
        assert self._contour is None
        if contour is not None:
            contour = reference(contour)
        self._contour = contour

    # Glyph

    glyph = dynamicProperty("glyph", "The segment's parent glyph.")

    def _get_glyph(self):
        if self._contour is None:
            return None
        return self.contour.glyph

    # Layer

    layer = dynamicProperty("layer", "The segment's parent layer.")

    def _get_layer(self):
        if self._contour is None:
            return None
        return self.glyph.layer

    # Font

    font = dynamicProperty("font", "The segment's parent font.")

    def _get_font(self):
        if self._contour is None:
            return None
        return self.glyph.font

    # --------
    # Equality
    # --------

    def __eq__(self, other):
        """
        The :meth:`BaseObject.__eq__` method can't be used here
        because the :class:`BaseContour` implementation contructs
        segment objects without assigning an underlying ``naked``
        object. Therefore, comparisons will always fail. This
        method overrides the base method and compares the
        :class:`BasePoint` contained by the segment.

        Subclasses may override this method.
        """
        if isinstance(other, self.__class__):
            return self.points == other.points
        return NotImplemented

    # --------------
    # Identification
    # --------------

    index = dynamicProperty("base_index",
                            ("The index of the segment within the ordered "
                             "list of the parent contour's segments.")
                            )

    def _get_base_index(self):
        if self.contour is None:
            return None
        value = self._get_index()
        value = normalizers.normalizeIndex(value)
        return value

    def _get_index(self):
        """
        Subclasses may override this method.
        """
        contour = self.contour
        value = contour.segments.index(self)
        return value

    # ----------
    # Attributes
    # ----------

    type = dynamicProperty("base_type",
                           ("The segment type. The possible types are "
                            "move, line, curve, qcurve.")
                           )

    def _get_base_type(self):
        value = self._get_type()
        value = normalizers.normalizeSegmentType(value)
        return value

    def _set_base_type(self, value):
        value = normalizers.normalizeSegmentType(value)
        self._set_type(value)

    def _get_type(self):
        """
        Subclasses may override this method.
        """
        value = self.onCurve.type
        return value

    def _set_type(self, newType):
        """
        Subclasses may override this method.
        """
        oldType = self.type
        if oldType == newType:
            return
        contour = self.contour
        if contour is None:
            raise FontPartsError("The segment does not belong to a contour.")
        # converting line <-> move
        if newType in ("move", "line") and oldType in ("move", "line"):
            pass
        # converting to a move or line
        elif newType not in ("curve", "qcurve"):
            offCurves = self.offCurve
            for point in offCurves:
                contour.removePoint(point)
        # converting a line/move to a curve/qcurve
        else:
            segments = contour.segments
            i = segments.index(self)
            prev = segments[i - 1].onCurve
            on = self.onCurve
            x = on.x
            y = on.y
            points = contour.points
            i = points.index(on)
            contour.insertPoint(i, (x, y), "offcurve")
            off2 = contour.points[i]
            contour.insertPoint(i, (prev.x, prev.y), "offcurve")
            off1 = contour.points[i]
            del self._points
            self._setPoints((off1, off2, on))
        self.onCurve.type = newType

    smooth = dynamicProperty("base_smooth",
                             ("Boolean indicating if the segment is "
                              "smooth or not.")
                             )

    def _get_base_smooth(self):
        value = self._get_smooth()
        value = normalizers.normalizeBoolean(value)
        return value

    def _set_base_smooth(self, value):
        value = normalizers.normalizeBoolean(value)
        self._set_smooth(value)

    def _get_smooth(self):
        """
        Subclasses may override this method.
        """
        return self.onCurve.smooth

    def _set_smooth(self, value):
        """
        Subclasses may override this method.
        """
        self.onCurve.smooth = value

    # ------
    # Points
    # ------

    def __getitem__(self, index):
        return self._getItem(index)

    def _getItem(self, index):
        """
        Subclasses may override this method.
        """
        return self.points[index]

    def __iter__(self):
        return self._iterPoints()

    def _iterPoints(self, **kwargs):
        """
        Subclasses may override this method.
        """
        points = self.points
        count = len(points)
        index = 0
        while count:
            yield points[index]
            count -= 1
            index += 1

    def __len__(self):
        return self._len()

    def _len(self, **kwargs):
        """
        Subclasses may override this method.
        """
        return len(self.points)

    points = dynamicProperty("base_points",
                             "A list of points in the segment.")

    def _get_base_points(self):
        return tuple(self._get_points())

    def _get_points(self):
        """
        Subclasses may override this method.
        """
        if not hasattr(self, "_points"):
            return tuple()
        return tuple(self._points)

    onCurve = dynamicProperty("base_onCurve",
                              "The on curve point in the segment.")

    def _get_base_onCurve(self):
        return self._get_onCurve()

    def _get_onCurve(self):
        """
        Subclasses may override this method.
        """
        return self.points[-1]

    offCurve = dynamicProperty("base_offCurve",
                               "The off curve points in the segment.")

    def _get_base_offCurve(self):
        """
        Subclasses may override this method.
        """
        return self._get_offCurve()

    def _get_offCurve(self):
        """
        Subclasses may override this method.
        """
        return self.points[:-1]

    # --------------
    # Transformation
    # --------------

    def _transformBy(self, matrix, **kwargs):
        """
        Subclasses may override this method.
        """
        for point in self.points:
            point.transformBy(matrix)

    # -------------
    # Interpolation
    # -------------

    compatibilityReporterClass = SegmentCompatibilityReporter

    def isCompatible(self, other):
        """
        Evaluate interpolation compatibility with **other**. ::

            >>> compatible, report = self.isCompatible(otherSegment)
            >>> compatible
            False
            >>> compatible
            [Fatal] Segment: [0] + [0]
            [Fatal] Segment: [0] is line | [0] is move
            [Fatal] Segment: [1] + [1]
            [Fatal] Segment: [1] is line | [1] is qcurve

        This will return a ``bool`` indicating if the segment is
        compatible for interpolation with **other** and a
        :ref:`type-string` of compatibility notes.
        """
        return super(BaseSegment, self).isCompatible(other, BaseSegment)

    def _isCompatible(self, other, reporter):
        """
        This is the environment implementation of
        :meth:`BaseSegment.isCompatible`.

        Subclasses may override this method.
        """
        segment1 = self
        segment2 = other
        # type
        if segment1.type != segment2.type:
            # line <-> curve can be converted
            if set((segment1.type, segment2.type)) != set(("curve", "line")):
                reporter.typeDifference = True
                reporter.fatal = True

    # ----
    # Misc
    # ----

    def round(self):
        """
        Round coordinates in all points.
        """
        for point in self.points:
            point.round()
Esempio n. 24
0
class BaseAnchor(BaseObject, TransformationMixin, DeprecatedAnchor,
                 RemovedAnchor, PointPositionMixin, InterpolationMixin,
                 SelectionMixin, IdentifierMixin):
    """
    An anchor object. This object is almost always
    created with :meth:`BaseGlyph.appendAnchor`.
    An orphan anchor can be created like this::

        >>> anchor = RAnchor()
    """
    def _reprContents(self):
        contents = [
            ("({x}, {y})".format(x=self.x, y=self.y)),
        ]
        if self.name is not None:
            contents.append("name='%s'" % self.name)
        if self.color:
            contents.append("color=%r" % str(self.color))
        return contents

    # ----
    # Copy
    # ----

    copyAttributes = ("x", "y", "name", "color")

    # -------
    # Parents
    # -------

    # Glyph

    _glyph = None

    glyph = dynamicProperty("glyph", "The anchor's parent :class:`BaseGlyph`.")

    def _get_glyph(self):
        if self._glyph is None:
            return None
        return self._glyph()

    def _set_glyph(self, glyph):
        if self._glyph is not None:
            raise AssertionError("glyph for anchor already set")
        if glyph is not None:
            glyph = reference(glyph)
        self._glyph = glyph

    # Layer

    layer = dynamicProperty("layer", "The anchor's parent :class:`BaseLayer`.")

    def _get_layer(self):
        if self._glyph is None:
            return None
        return self.glyph.layer

    # Font

    font = dynamicProperty("font", "The anchor's parent :class:`BaseFont`.")

    def _get_font(self):
        if self._glyph is None:
            return None
        return self.glyph.font

    # --------
    # Position
    # --------

    # x

    x = dynamicProperty(
        "base_x", """
        The x coordinate of the anchor.
        It must be an :ref:`type-int-float`. ::

            >>> anchor.x
            100
            >>> anchor.x = 101
        """)

    def _get_base_x(self):
        value = self._get_x()
        value = normalizers.normalizeX(value)
        return value

    def _set_base_x(self, value):
        value = normalizers.normalizeX(value)
        self._set_x(value)

    def _get_x(self):
        """
        This is the environment implementation of
        :attr:`BaseAnchor.x`. This must return an
        :ref:`type-int-float`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_x(self, value):
        """
        This is the environment implementation of
        :attr:`BaseAnchor.x`. **value** will be
        an :ref:`type-int-float`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # y

    y = dynamicProperty(
        "base_y", """
        The y coordinate of the anchor.
        It must be an :ref:`type-int-float`. ::

            >>> anchor.y
            100
            >>> anchor.y = 101
        """)

    def _get_base_y(self):
        value = self._get_y()
        value = normalizers.normalizeY(value)
        return value

    def _set_base_y(self, value):
        value = normalizers.normalizeY(value)
        self._set_y(value)

    def _get_y(self):
        """
        This is the environment implementation of
        :attr:`BaseAnchor.y`. This must return an
        :ref:`type-int-float`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_y(self, value):
        """
        This is the environment implementation of
        :attr:`BaseAnchor.y`. **value** will be
        an :ref:`type-int-float`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # --------------
    # Identification
    # --------------

    # index

    index = dynamicProperty(
        "base_index", """
        The index of the anchor within the ordered
        list of the parent glyph's anchors. This
        attribute is read only. ::

            >>> anchor.index
            0
        """)

    def _get_base_index(self):
        value = self._get_index()
        value = normalizers.normalizeIndex(value)
        return value

    def _get_index(self):
        """
        Get the anchor's index.
        This must return an ``int``.

        Subclasses may override this method.
        """
        glyph = self.glyph
        if glyph is None:
            return None
        return glyph.anchors.index(self)

    # name

    name = dynamicProperty(
        "base_name", """
        The name of the anchor. This will be a
        :ref:`type-string` or ``None``.

            >>> anchor.name
            'my anchor'
            >>> anchor.name = None
        """)

    def _get_base_name(self):
        value = self._get_name()
        value = normalizers.normalizeAnchorName(value)
        return value

    def _set_base_name(self, value):
        value = normalizers.normalizeAnchorName(value)
        self._set_name(value)

    def _get_name(self):
        """
        This is the environment implementation of
        :attr:`BaseAnchor.name`. This must return a
        :ref:`type-string` or ``None``. The returned
        value will be normalized with
        :func:`normalizers.normalizeAnchorName`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_name(self, value):
        """
        This is the environment implementation of
        :attr:`BaseAnchor.name`. **value** will be
        a :ref:`type-string` or ``None``. It will
        have been normalized with
        :func:`normalizers.normalizeAnchorName`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # color

    color = dynamicProperty(
        "base_color", """
        The anchor's color. This will be a
        :ref:`type-color` or ``None``. ::

            >>> anchor.color
            None
            >>> anchor.color = (1, 0, 0, 0.5)
        """)

    def _get_base_color(self):
        value = self._get_color()
        if value is not None:
            value = normalizers.normalizeColor(value)
            value = Color(value)
        return value

    def _set_base_color(self, value):
        if value is not None:
            value = normalizers.normalizeColor(value)
        self._set_color(value)

    def _get_color(self):
        """
        This is the environment implementation of
        :attr:`BaseAnchor.color`. This must return
        a :ref:`type-color` or ``None``. The
        returned value will be normalized with
        :func:`normalizers.normalizeColor`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_color(self, value):
        """
        This is the environment implementation of
        :attr:`BaseAnchor.color`. **value** will
        be a :ref:`type-color` or ``None``.
        It will have been normalized with
        :func:`normalizers.normalizeColor`.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # --------------
    # Transformation
    # --------------

    def _transformBy(self, matrix, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseAnchor.transformBy`.

        **matrix** will be a :ref:`type-transformation`.
        that has been normalized with
        :func:`normalizers.normalizeTransformationMatrix`.

        Subclasses may override this method.
        """
        t = transform.Transform(*matrix)
        x, y = t.transformPoint((self.x, self.y))
        self.x = x
        self.y = y

    # -------------
    # Interpolation
    # -------------

    compatibilityReporterClass = AnchorCompatibilityReporter

    def isCompatible(self, other):
        """
        Evaluate interpolation compatibility with **other**. ::

            >>> compatible, report = self.isCompatible(otherAnchor)
            >>> compatible
            True
            >>> compatible
            [Warning] Anchor: "left" + "right"
            [Warning] Anchor: "left" has name left | "right" has name right

        This will return a ``bool`` indicating if the anchor is
        compatible for interpolation with **other** and a
        :ref:`type-string` of compatibility notes.
        """
        return super(BaseAnchor, self).isCompatible(other, BaseAnchor)

    def _isCompatible(self, other, reporter):
        """
        This is the environment implementation of
        :meth:`BaseAnchor.isCompatible`.

        Subclasses may override this method.
        """
        anchor1 = self
        anchor2 = other
        # base names
        if anchor1.name != anchor2.name:
            reporter.nameDifference = True
            reporter.warning = True

    # -------------
    # Normalization
    # -------------

    def round(self):
        """
        Round the anchor's coordinate.

            >>> anchor.round()

        This applies to the following:

        * x
        * y
        """
        self._round()

    def _round(self):
        """
        This is the environment implementation of
        :meth:`BaseAnchor.round`.

        Subclasses may override this method.
        """
        self.x = normalizers.normalizeRounding(self.x)
        self.y = normalizers.normalizeRounding(self.y)
Esempio n. 25
0
class BaseContour(
        BaseObject,
        TransformationMixin,
        InterpolationMixin,
        SelectionMixin,
        IdentifierMixin,
        DeprecatedContour,
        RemovedContour
     ):

    segmentClass = None
    bPointClass = None

    def _reprContents(self):
        contents = []
        if self.identifier is not None:
            contents.append("identifier='%r'" % self.identifier)
        if self.glyph is not None:
            contents.append("in glyph")
            contents += self.glyph._reprContents()
        return contents

    def copyData(self, source):
        super(BaseContour, self).copyData(source)
        for sourcePoint in source.points:
            self.appendPoint((0, 0))
            selfPoint = self.points[-1]
            selfPoint.copyData(sourcePoint)

    # -------
    # Parents
    # -------

    # Glyph

    _glyph = None

    glyph = dynamicProperty("glyph",
                            "The contour's parent :class:`BaseGlyph`.")

    def _get_glyph(self):
        if self._glyph is None:
            return None
        return self._glyph()

    def _set_glyph(self, glyph):
        if self._glyph is not None:
            raise AssertionError("glyph for contour already set")
        if glyph is not None:
            glyph = reference(glyph)
        self._glyph = glyph

    # Font

    font = dynamicProperty("font", "The contour's parent font.")

    def _get_font(self):
        if self._glyph is None:
            return None
        return self.glyph.font

    # Layer

    layer = dynamicProperty("layer", "The contour's parent layer.")

    def _get_layer(self):
        if self._glyph is None:
            return None
        return self.glyph.layer

    # --------------
    # Identification
    # --------------

    # index

    index = dynamicProperty(
        "base_index",
        """
        The index of the contour within the parent glyph's contours.

            >>> contour.index
            1
            >>> contour.index = 0

        The value will always be a :ref:`type-int`.
        """
    )

    def _get_base_index(self):
        glyph = self.glyph
        if glyph is None:
            return None
        value = self._get_index()
        value = normalizers.normalizeIndex(value)
        return value

    def _set_base_index(self, value):
        glyph = self.glyph
        if glyph is None:
            raise FontPartsError("The contour does not belong to a glyph.")
        value = normalizers.normalizeIndex(value)
        contourCount = len(glyph.contours)
        if value < 0:
            value = -(value % contourCount)
        if value >= contourCount:
            value = contourCount
        self._set_index(value)

    def _get_index(self):
        """
        Subclasses may override this method.
        """
        glyph = self.glyph
        return glyph.contours.index(self)

    def _set_index(self, value):
        """
        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # identifier

    def getIdentifierForPoint(self, point):
        """
        Create a unique identifier for and assign it to ``point``.
        If the point already has an identifier, the existing
        identifier will be returned.

            >>> contour.getIdentifierForPoint(point)
            'ILHGJlygfds'

        ``point`` must be a :class:`BasePoint`. The returned value
        will be a :ref:`type-identifier`.
        """
        point = normalizers.normalizePoint(point)
        return self._getIdentifierforPoint(point)

    def _getIdentifierforPoint(self, point):
        """
        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # ----
    # Pens
    # ----

    def draw(self, pen):
        """
        Draw the contour's outline data to the given :ref:`type-pen`.

            >>> contour.draw(pen)
        """
        self._draw(pen)

    def _draw(self, pen, **kwargs):
        """
        Subclasses may override this method.
        """
        from ufoLib.pointPen import PointToSegmentPen
        adapter = PointToSegmentPen(pen)
        self.drawPoints(adapter)

    def drawPoints(self, pen):
        """
        Draw the contour's outline data to the given :ref:`type-point-pen`.

            >>> contour.drawPoints(pointPen)
        """
        self._drawPoints(pen)

    def _drawPoints(self, pen, **kwargs):
        """
        Subclasses may override this method.
        """
        # The try: ... except TypeError: ...
        # handles backwards compatibility with
        # point pens that have not been upgraded
        # to point pen protocol 2.
        try:
            pen.beginPath(self.identifier)
        except TypeError:
            pen.beginPath()
        for point in self.points:
            typ = point.type
            if typ == "offcurve":
                typ = None
            try:
                pen.addPoint(pt=(point.x, point.y), segmentType=typ,
                             smooth=point.smooth, name=point.name,
                             identifier=point.identifier)
            except TypeError:
                pen.addPoint(pt=(point.x, point.y), segmentType=typ,
                             smooth=point.smooth, name=point.name)
        pen.endPath()

    # ------------------
    # Data normalization
    # ------------------

    def autoStartSegment(self):
        """
        Automatically calculate and set the first segment
        in this contour.

        The behavior of this may vary accross environments.
        """
        self._autoStartSegment()

    def _autoStartSegment(self, **kwargs):
        """
        Subclasses may override this method.

        XXX port this from robofab
        """
        startIndex = 0
        startSegment = self.segments[0]
        for i in range(len(self.segments)):
            segment = self.segments[i]
            startOn = startSegment.onCurve
            on = segment.onCurve
            if on.y <= startOn.y:
                if on.y == startOn.y:
                    if on.x < startOn.x:
                        startSegment = segment
                        startIndex = i
                else:
                    startSegment = segment
                    startIndex = i
        if startIndex != 0:
            self.setStartSegment(startIndex)

    def round(self):
        """
        Round coordinates in all points to integers.
        """
        self._round()

    def _round(self, **kwargs):
        """
        Subclasses may override this method.
        """
        for point in self.points:
            point.round()

    # --------------
    # Transformation
    # --------------

    def _transformBy(self, matrix, **kwargs):
        """
        Subclasses may override this method.
        """
        for point in self.points:
            point.transformBy(matrix)

    # -------------
    # Interpolation
    # -------------

    compatibilityReporterClass = ContourCompatibilityReporter

    def isCompatible(self, other):
        """
        Evaluate interpolation compatibility with **other**. ::

            >>> compatible, report = self.isCompatible(otherContour)
            >>> compatible
            False
            >>> compatible
            [Fatal] Contour: [0] + [0]
            [Fatal] Contour: [0] contains 4 segments | [0] contains 3 segments
            [Fatal] Contour: [0] is closed | [0] is open

        This will return a ``bool`` indicating if the contour is
        compatible for interpolation with **other** and a
        :ref:`type-string` of compatibility notes.
        """
        return super(BaseContour, self).isCompatible(other, BaseContour)

    def _isCompatible(self, other, reporter):
        """
        This is the environment implementation of
        :meth:`BaseContour.isCompatible`.

        Subclasses may override this method.
        """
        contour1 = self
        contour2 = other
        # open/closed
        if contour1.open != contour2.open:
            reporter.openDifference = True
        # direction
        if contour1.clockwise != contour2.clockwise:
            reporter.directionDifference = True
        # segment count
        if len(contour1) != len(contour2.segments):
            reporter.segmentCountDifference = True
            reporter.fatal = True
        # segment pairs
        for i in range(min(len(contour1), len(contour2))):
            segment1 = contour1[i]
            segment2 = contour2[i]
            segmentCompatibility = segment1.isCompatible(segment2)[1]
            if segmentCompatibility.fatal or segmentCompatibility.warning:
                if segmentCompatibility.fatal:
                    reporter.fatal = True
                if segmentCompatibility.warning:
                    reporter.warning = True
                reporter.segments.append(segmentCompatibility)

    # ----
    # Open
    # ----

    open = dynamicProperty("base_open",
                           "Boolean indicating if the contour is open.")

    def _get_base_open(self):
        value = self._get_open()
        value = normalizers.normalizeBoolean(value)
        return value

    def _get_open(self):
        """
        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # ---------
    # Direction
    # ---------

    clockwise = dynamicProperty("base_clockwise",
                                ("Boolean indicating if the contour's "
                                 "winding direction is clockwise."))

    def _get_base_clockwise(self):
        value = self._get_clockwise()
        value = normalizers.normalizeBoolean(value)
        return value

    def _set_base_clockwise(self, value):
        value = normalizers.normalizeBoolean(value)
        self._set_clockwise(value)

    def _get_clockwise(self):
        """
        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _set_clockwise(self, value):
        """
        Subclasses may override this method.
        """
        if self.clockwise != value:
            self.reverse()

    def reverse(self):
        """
        Reverse the direction of the contour.
        """
        self._reverseContour()

    def _reverse(self, **kwargs):
        """
        Subclasses may override this method.
        """
        self.raiseNotImplementedError()

    # ------------------------
    # Point and Contour Inside
    # ------------------------

    def pointInside(self, point):
        """
        Determine if ``point`` is in the black or white of the contour.

            >>> contour.pointInside((40, 65))
            True

        ``point`` must be a :ref:`type-coordinate`.
        """
        point = normalizers.normalizeCoordinateTuple(point)
        return self._pointInside(point)

    def _pointInside(self, point):
        """
        Subclasses may override this method.
        """
        from fontTools.pens.pointInsidePen import PointInsidePen
        pen = PointInsidePen(glyphSet=None, testPoint=point, evenOdd=False)
        self.draw(pen)
        return pen.getResult()

    def contourInside(self, otherContour):
        """
        Determine if ``otherContour`` is in the black or white of this contour.

            >>> contour.contourInside(otherContour)
            True

        ``contour`` must be a :class:`BaseContour`.
        """
        otherContour = normalizers.normalizeContour(otherContour)
        return self._contourInside(otherContour)

    def _contourInside(self, otherContour):
        """
        Subclasses may override this method.
        """
        self.raiseNotImplementedError()

    # ---------------
    # Bounds and Area
    # ---------------

    bounds = dynamicProperty("bounds",
                             ("The bounds of the contour: "
                              "(xMin, yMin, xMax, yMax) or None."))

    def _get_base_bounds(self):
        value = self._get_bounds()
        if value is not None:
            value = normalizers.normalizeBoundingBox(value)
        return value

    def _get_bounds(self):
        """
        Subclasses may override this method.
        """
        from fontTools.pens.boundsPen import BoundsPen
        pen = BoundsPen(self.layer)
        self.draw(pen)
        return pen.bounds

    area = dynamicProperty("area",
                           ("The area of the contour: "
                            "A positive number or None."))

    def _get_base_area(self):
        value = self._get_area()
        if value is not None:
            value = normalizers.normalizeArea(value)
        return value

    def _get_area(self):
        """
        Subclasses may override this method.
        """
        from fontTools.pens.areaPen import AreaPen
        pen = AreaPen(self.layer)
        self.draw(pen)
        return abs(pen.value)

    # --------
    # Segments
    # --------

    # The base class implements the full segment interaction API.
    # Subclasses do not need to override anything within the contour
    # other than registering segmentClass. Subclasses may choose to
    # implement this API independently if desired.

    def _setContourInSegment(self, segment):
        if segment.contour is None:
            segment.contour = self

    segments = dynamicProperty("segments")

    def _get_segments(self):
        """
        Subclasses may override this method.
        """
        points = list(self.points)
        segments = [[]]
        lastWasOffCurve = False
        firstIsMove = points[0].type == "move"
        for point in points:
            segments[-1].append(point)
            if point.type != "offcurve":
                segments.append([])
            lastWasOffCurve = point.type == "offcurve"
        if len(segments[-1]) == 0:
            del segments[-1]
        if lastWasOffCurve and firstIsMove:
            # ignore trailing off curves
            del segments[-1]
        if lastWasOffCurve and not firstIsMove:
            segment = segments.pop(-1)
            if len(segments[0]) != 1:
                raise AssertionError("Length of segments[0] is not 1")
            segment.append(segments[0][0])
            del segments[0]
            segments.append(segment)
        if not lastWasOffCurve and not firstIsMove:
            segment = segments.pop(0)
            segments.append(segment)
        # wrap into segments
        wrapped = []
        for points in segments:
            s = self.segmentClass()
            s._setPoints(points)
            self._setContourInSegment(s)
            wrapped.append(s)
        return wrapped

    def __getitem__(self, index):
        return self.segments[index]

    def __iter__(self):
        return self._iterSegments()

    def _iterSegments(self):
        segments = self.segments
        count = len(segments)
        index = 0
        while count:
            yield segments[index]
            count -= 1
            index += 1

    def __len__(self):
        return self._len__segments()

    def _len__segments(self, **kwargs):
        """
        Subclasses may override this method.
        """
        return len(self.segments)

    def appendSegment(self, type=None, points=None, smooth=False, segment=None):
        """
        Append a segment to the contour.
        """
        if segment is not None:
            if type is not None:
                type = segment.type
            if points is None:
                points = [(point.x, point.y) for point in segment.points]
            smooth = segment.smooth
        type = normalizers.normalizeSegmentType(type)
        pts = []
        for pt in points:
            pt = normalizers.normalizeCoordinateTuple(pt)
            pts.append(pt)
        points = pts
        smooth = normalizers.normalizeBoolean(smooth)
        self._appendSegment(type=type, points=points, smooth=smooth)

    def _appendSegment(self, type=None, points=None, smooth=False, **kwargs):
        """
        Subclasses may override this method.
        """
        self._insertSegment(len(self), type=type, points=points,
                            smooth=smooth, **kwargs)

    def insertSegment(self, index, type=None, points=None, smooth=False, segment=None):
        """
        Insert a segment into the contour.
        """
        if segment is not None:
            if type is not None:
                type = segment.type
            if points is None:
                points = [(point.x, point.y) for point in segment.points]
            smooth = segment.smooth
        index = normalizers.normalizeIndex(index)
        type = normalizers.normalizeSegmentType(type)
        pts = []
        for pt in points:
            pt = normalizers.normalizeCoordinateTuple(pt)
            pts.append(pt)
        points = pts
        smooth = normalizers.normalizeBoolean(smooth)
        self._insertSegment(index=index, type=type,
                            points=points, smooth=smooth)

    def _insertSegment(self, index=None, type=None, points=None,
                       smooth=False, **kwargs):
        """
        Subclasses may override this method.
        """
        onCurve = points[-1]
        offCurve = points[:-1]
        ptCount = sum([len(self.segments[s].points) for s in range(index)])
        self.insertPoint(ptCount, onCurve, type=type, smooth=smooth)
        for offCurvePoint in reversed(offCurve):
            self.insertPoint(ptCount, offCurvePoint, type="offcurve")

    def removeSegment(self, segment, preserveCurve=False):
        """
        Remove segment from the contour.
        If ``preserveCurve`` is set to ``True`` an attempt
        will be made to preserve the shape of the curve
        if the environment supports that functionality.
        """
        if not isinstance(segment, int):
            segment = self.segments.index(segment)
        segment = normalizers.normalizeIndex(segment)
        if segment >= self._len__segments():
            raise ValueError("No segment located at index %d." % segment)
        preserveCurve = normalizers.normalizeBoolean(preserveCurve)
        self._removeSegment(segment, preserveCurve)

    def _removeSegment(self, segment, preserveCurve, **kwargs):
        """
        segment will be a valid segment index.
        preserveCurve will be a boolean.

        Subclasses may override this method.
        """
        segment = self.segments[segment]
        for point in segment.points:
            self.removePoint(point, preserveCurve)

    def setStartSegment(self, segment):
        """
        Set the first segment on the contour.
        segment can be a segment object or an index.
        """
        segments = self.segments
        if not isinstance(segment, int):
            segmentIndex = segments.index(segment)
        else:
            segmentIndex = segment
        if len(self.segments) < 2:
            return
        if segmentIndex == 0:
            return
        if segmentIndex >= len(segments):
            raise ValueError(("The contour does not contain a segment "
                              "at index %d" % segmentIndex))
        self._setStartSegment(segmentIndex)

    def _setStartSegment(self, segmentIndex, **kwargs):
        """
        Subclasses may override this method.
        """
        segments = self.segments
        oldStart = self.segments[0]
        oldLast = self.segments[-1]
        # If the contour ends with a curve on top of a move,
        # delete the move.
        if oldLast.type == "curve" or oldLast.type == "qcurve":
            startOn = oldStart.onCurve
            lastOn = oldLast.onCurve
            if startOn.x == lastOn.x and startOn.y == lastOn.y:
                self.removeSegment(0)
                # Shift new the start index.
                segmentIndex = segmentIndex - 1
                segments = self.segments
        # If the first point is a move, convert it to a line.
        if segments[0].type == "move":
            segments[0].type = "line"
        # Reorder the points internally.
        segments = segments[segmentIndex:] + segments[:segmentIndex]
        points = []
        for segment in segments:
            for point in segment:
                points.append(((point.x, point.y), point.type,
                               point.smooth, point.name, point.identifier))
        # Clear the points.
        for point in self.points:
            self.removePoint(point)
        # Add the points.
        for point in points:
            position, type, smooth, name, identifier = point
            self.appendPoint(
                position,
                type=type,
                smooth=smooth,
                name=name,
                identifier=identifier
            )

    # -------
    # bPoints
    # -------

    bPoints = dynamicProperty("bPoints")

    def _get_bPoints(self):
        bPoints = []
        for point in self.points:
            if point.type not in ("move", "line", "curve"):
                continue
            bPoint = self.bPointClass()
            bPoint.contour = self
            bPoint._setPoint(point)
            bPoints.append(bPoint)
        return tuple(bPoints)

    def appendBPoint(self, type=None, anchor=None, bcpIn=None, bcpOut=None, bPoint=None):
        """
        Append a bPoint to the contour.
        """
        if bPoint is not None:
            if type is None:
                type = bPoint.type
            if anchor is None:
                anchor = bPoint.anchor
            if bcpIn is None:
                bcpIn = bPoint.bcpIn
            if bcpOut is None:
                bcpOut = bPoint.bcpOut
        type = normalizers.normalizeBPointType(type)
        anchor = normalizers.normalizeCoordinateTuple(anchor)
        if bcpIn is None:
            bcpIn = (0, 0)
        bcpIn = normalizers.normalizeCoordinateTuple(bcpIn)
        if bcpOut is None:
            bcpOut = (0, 0)
        bcpOut = normalizers.normalizeCoordinateTuple(bcpOut)
        self._appendBPoint(type, anchor, bcpIn=bcpIn, bcpOut=bcpOut)

    def _appendBPoint(self, type, anchor, bcpIn=None, bcpOut=None, **kwargs):
        """
        Subclasses may override this method.
        """
        self.insertBPoint(
            len(self.bPoints),
            type,
            anchor,
            bcpIn=bcpIn,
            bcpOut=bcpOut
        )

    def insertBPoint(self, index, type=None, anchor=None, bcpIn=None, bcpOut=None, bPoint=None):
        """
        Insert a bPoint at index in the contour.
        """
        if bPoint is not None:
            if type is None:
                type = bPoint.type
            if anchor is None:
                anchor = bPoint.anchor
            if bcpIn is None:
                bcpIn = bPoint.bcpIn
            if bcpOut is None:
                bcpOut = bPoint.bcpOut
        index = normalizers.normalizeIndex(index)
        type = normalizers.normalizeBPointType(type)
        anchor = normalizers.normalizeCoordinateTuple(anchor)
        if bcpIn is None:
            bcpIn = (0, 0)
        bcpIn = normalizers.normalizeCoordinateTuple(bcpIn)
        if bcpOut is None:
            bcpOut = (0, 0)
        bcpOut = normalizers.normalizeCoordinateTuple(bcpOut)
        self._insertBPoint(index=index, type=type, anchor=anchor,
                           bcpIn=bcpIn, bcpOut=bcpOut)

    def _insertBPoint(self, index, type, anchor, bcpIn, bcpOut, **kwargs):
        """
        Subclasses may override this method.
        """
        segments = self.segments
        # insert a curve point that we can work with
        nextSegment = segments[index]
        if nextSegment.type not in ("move", "line", "curve"):
            raise ValueError("Unknown segment type (%s) in contour."
                             % nextSegment.type)
        if nextSegment.type == "move":
            prevSegment = segments[index - 1]
            prevOn = prevSegment.onCurve
            if bcpIn != (0, 0):
                new = self.appendSegment(
                    "curve",
                    [(prevOn.x, prevOn.y), absoluteBCPIn(anchor, bcpIn),
                     anchor],
                    smooth=False
                )
                if type == "curve":
                    new.smooth = True
            else:
                new = self.appendSegment(
                    "line",
                    [anchor],
                    smooth=False
                )
            # if the user wants an outgoing bcp, we must
            # add a curve ontop of the move
            if bcpOut != (0, 0):
                nextOn = nextSegment.onCurve
                self.appendSegment(
                    "curve",
                    [absoluteBCPOut(anchor, bcpOut), (nextOn.x, nextOn.y),
                     (nextOn.x, nextOn.y)],
                    smooth=False
                )
        else:
            # handle the bcps
            if nextSegment.type != "curve":
                prevSegment = segments[index - 1]
                prevOn = prevSegment.onCurve
                prevOutX, prevOutY = (prevOn.x, prevOn.y)
            else:
                prevOut = nextSegment.offCurve[0]
                prevOutX, prevOutY = (prevOut.x, prevOut.y)
            newSegment = self.insertSegment(
                index,
                type="curve",
                points=[(prevOutX, prevOutY), anchor, anchor],
                smooth=False
            )
            segments = self.segments
            p = index - 1
            if p < 0:
                p = -1
            prevSegment = segments[p]
            n = index + 1
            if n >= len(segments):
                n = 0
            nextSegment = segments[n]
            if nextSegment.type == "move":
                raise FontPartsError(("still working out curving at the "
                                      "end of a contour"))
            elif nextSegment.type == "qcurve":
                return
            # set the new incoming bcp
            newIn = newSegment.offCurve[1]
            nIX, nIY = absoluteBCPIn(anchor, bcpIn)
            newIn.x = nIX
            newIn.y = nIY
            # set the new outgoing bcp
            hasCurve = True
            if nextSegment.type != "curve":
                if bcpOut != (0, 0):
                    nextSegment.type = "curve"
                    hasCurve = True
                else:
                    hasCurve = False
            if hasCurve:
                newOut = nextSegment.offCurve[0]
                nOX, nOY = absoluteBCPOut(anchor, bcpOut)
                newOut.x = nOX
                newOut.y = nOY
            # now check to see if we can convert the curve
            # segment to a line segment
            newAnchor = newSegment.onCurve
            newA = newSegment.offCurve[0]
            newB = newSegment.offCurve[1]
            prevAnchor = prevSegment.onCurve
            if (
                (prevAnchor.x, prevAnchor.y) == (newA.x, newA.y) and
                (newAnchor.x, newAnchor.y) == (newB.x, newB.y)
               ):
                newSegment.type = "line"
            # the user wants a smooth segment
            if type == "curve":
                newSegment.smooth = True

    def removeBPoint(self, bPoint):
        """
        Remove the bpoint from the contour.
        bpoint can be a point object or an index.
        """
        if not isinstance(bPoint, int):
            bPoint = bPoint.index
        bPoint = normalizers.normalizeIndex(bPoint)
        if bPoint >= self._len__points():
            raise ValueError("No bPoint located at index %d." % bPoint)
        self._removeBPoint(bPoint)

    def _removeBPoint(self, index, **kwargs):
        """
        index will be a valid index.

        Subclasses may override this method.
        """
        bPoint = self.bPoints[index]

        nextSegment = bPoint._nextSegment
        offCurves = nextSegment.offCurve
        if offCurves:
            offCurve = offCurves[0]
            self.removePoint(offCurve)

        segment = bPoint._segment
        offCurves = segment.offCurve
        if offCurves:
            offCurve = offCurves[-1]
            self.removePoint(offCurve)

        self.removePoint(bPoint._point)

    # ------
    # Points
    # ------

    def _setContourInPoint(self, point):
        if point.contour is None:
            point.contour = self

    points = dynamicProperty("points")

    def _get_points(self):
        """
        Subclasses may override this method.
        """
        return tuple([self._getitem__points(i)
                     for i in range(self._len__points())])

    def _len__points(self):
        return self._lenPoints()

    def _lenPoints(self, **kwargs):
        """
        This must return an integer indicating
        the number of points in the contour.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _getitem__points(self, index):
        index = normalizers.normalizeIndex(index)
        if index >= self._len__points():
            raise ValueError("No point located at index %d." % index)
        point = self._getPoint(index)
        self._setContourInPoint(point)
        return point

    def _getPoint(self, index, **kwargs):
        """
        This must return a wrapped point.

        index will be a valid index.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def _getPointIndex(self, point):
        for i, other in enumerate(self.points):
            if point == other:
                return i
        raise FontPartsError("The point could not be found.")

    def appendPoint(self, position=None, type="line", smooth=False, name=None, identifier=None, point=None):
        """
        Append a point to the contour.
        """
        if point is not None:
            if position is None:
                position = point.position
            type = point.type
            smooth = point.smooth
            if name is None:
                name = point.name
            if identifier is not None:
                identifier = point.identifier
        self.insertPoint(
            len(self.points),
            position=position,
            type=type,
            smooth=smooth,
            name=name,
            identifier=identifier
        )

    def insertPoint(self, index, position=None, type="line", smooth=False, name=None, identifier=None, point=None):
        """
        Insert a point into the contour.
        """
        if point is not None:
            if position is None:
                position = point.position
            type = point.type
            smooth = point.smooth
            if name is None:
                name = point.name
            if identifier is not None:
                identifier = point.identifier
        index = normalizers.normalizeIndex(index)
        position = normalizers.normalizeCoordinateTuple(position)
        type = normalizers.normalizePointType(type)
        smooth = normalizers.normalizeBoolean(smooth)
        if name is not None:
            name = normalizers.normalizePointName(name)
        if identifier is not None:
            identifier = normalizers.normalizeIdentifier(identifier)
        self._insertPoint(
            index,
            position=position,
            type=type,
            smooth=smooth,
            name=name,
            identifier=identifier
        )

    def _insertPoint(self, index, position, type="line",
                     smooth=False, name=None, identifier=None, **kwargs):
        """
        position will be a valid position (x, y).
        type will be a valid type.
        smooth will be a valid boolean.
        name will be a valid name or None.
        identifier will be a valid identifier or None.
        The identifier will not have been tested for uniqueness.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    def removePoint(self, point, preserveCurve=False):
        """
        Remove the point from the contour.
        point can be a point object or an index.
        If ``preserveCurve`` is set to ``True`` an attempt
        will be made to preserve the shape of the curve
        if the environment supports that functionality.
        """
        if not isinstance(point, int):
            point = self.points.index(point)
        point = normalizers.normalizeIndex(point)
        if point >= self._len__points():
            raise ValueError("No point located at index %d." % point)
        preserveCurve = normalizers.normalizeBoolean(preserveCurve)
        self._removePoint(point, preserveCurve)

    def _removePoint(self, index, preserveCurve, **kwargs):
        """
        index will be a valid index. preserveCurve will be a boolean.

        Subclasses must override this method.
        """
        self.raiseNotImplementedError()

    # ---------
    # Selection
    # ---------

    # segments

    selectedSegments = dynamicProperty(
        "base_selectedSegments",
        """
        A list of segments selected in the contour.

        Getting selected segment objects:

            >>> for segment in contour.selectedSegments:
            ...     segment.move((10, 20))

        Setting selected segment objects:

            >>> contour.selectedSegments = someSegments

        Setting also supports segment indexes:

            >>> contour.selectedSegments = [0, 2]
        """
    )

    def _get_base_selectedSegments(self):
        selected = tuple([normalizers.normalizeSegment(segment)
                         for segment in self._get_selectedSegments()])
        return selected

    def _get_selectedSegments(self):
        """
        Subclasses may override this method.
        """
        return self._getSelectedSubObjects(self.segments)

    def _set_base_selectedSegments(self, value):
        normalized = []
        for i in value:
            if isinstance(i, int):
                i = normalizers.normalizeSegmentIndex(i)
            else:
                i = normalizers.normalizeSegment(i)
            normalized.append(i)
        self._set_selectedSegments(normalized)

    def _set_selectedSegments(self, value):
        """
        Subclasses may override this method.
        """
        return self._setSelectedSubObjects(self.segments, value)

    # points

    selectedPoints = dynamicProperty(
        "base_selectedPoints",
        """
        A list of points selected in the contour.

        Getting selected point objects:

            >>> for point in contour.selectedPoints:
            ...     point.move((10, 20))

        Setting selected point objects:

            >>> contour.selectedPoints = somePoints

        Setting also supports point indexes:

            >>> contour.selectedPoints = [0, 2]
        """
    )

    def _get_base_selectedPoints(self):
        selected = tuple([normalizers.normalizePoint(point)
                         for point in self._get_selectedPoints()])
        return selected

    def _get_selectedPoints(self):
        """
        Subclasses may override this method.
        """
        return self._getSelectedSubObjects(self.points)

    def _set_base_selectedPoints(self, value):
        normalized = []
        for i in value:
            if isinstance(i, int):
                i = normalizers.normalizePointIndex(i)
            else:
                i = normalizers.normalizePoint(i)
            normalized.append(i)
        self._set_selectedPoints(normalized)

    def _set_selectedPoints(self, value):
        """
        Subclasses may override this method.
        """
        return self._setSelectedSubObjects(self.points, value)

    # bPoints

    selectedBPoints = dynamicProperty(
        "base_selectedBPoints",
        """
        A list of bPoints selected in the contour.

        Getting selected bPoint objects:

            >>> for bPoint in contour.selectedBPoints:
            ...     bPoint.move((10, 20))

        Setting selected bPoint objects:

            >>> contour.selectedBPoints = someBPoints

        Setting also supports bPoint indexes:

            >>> contour.selectedBPoints = [0, 2]
        """
    )

    def _get_base_selectedBPoints(self):
        selected = tuple([normalizers.normalizeBPoint(bPoint)
                         for bPoint in self._get_selectedBPoints()])
        return selected

    def _get_selectedBPoints(self):
        """
        Subclasses may override this method.
        """
        return self._getSelectedSubObjects(self.bPoints)

    def _set_base_selectedBPoints(self, value):
        normalized = []
        for i in value:
            if isinstance(i, int):
                i = normalizers.normalizeBPointIndex(i)
            else:
                i = normalizers.normalizeBPoint(i)
            normalized.append(i)
        self._set_selectedBPoints(normalized)

    def _set_selectedBPoints(self, value):
        """
        Subclasses may override this method.
        """
        return self._setSelectedSubObjects(self.bPoints, value)
Esempio n. 26
0
class BaseLib(BaseDict, DeprecatedLib, RemovedLib):

    keyNormalizer = normalizers.normalizeLibKey
    valueNormalizer = normalizers.normalizeLibValue

    def _reprContents(self):
        contents = []
        if self.glyph is not None:
            contents.append("in glyph")
            contents += self.glyph._reprContents()
        if self.font:
            contents.append("in font")
            contents += self.font._reprContents()
        return contents

    # -------
    # Parents
    # -------

    def getParent(self):
        """
        This is a backwards compatibility method.
        """
        glyph = self.glyph
        if glyph is not None:
            return glyph
        return self.font

    # Glyph

    _glyph = None

    glyph = dynamicProperty("glyph", "The lib's parent glyph.")

    def _get_glyph(self):
        if self._glyph is None:
            return None
        return self._glyph()

    def _set_glyph(self, glyph):
        assert self._font is None
        assert self._glyph is None or self._glyph() == glyph
        if glyph is not None:
            glyph = reference(glyph)
        self._glyph = glyph

    # Font

    _font = None

    font = dynamicProperty("font", "The lib's parent font.")

    def _get_font(self):
        if self._font is not None:
            return self._font()
        elif self._glyph is not None:
            return self.glyph.font
        return None

    def _set_font(self, font):
        assert self._font is None or self._font() == font
        assert self._glyph is None
        if font is not None:
            font = reference(font)
        self._font = font

    # Layer

    layer = dynamicProperty("layer", "The lib's parent layer.")

    def _get_layer(self):
        if self._glyph is None:
            return None
        return self.glyph.layer

    # ---------------------
    # RoboFab Compatibility
    # ---------------------

    def remove(self, key):
        del self[key]

    def asDict(self):
        d = {}
        for k, v in self.items():
            d[k] = v
        return d