Source code for fontParts.base.layer

from fontParts.base.base import (
    BaseObject,
    InterpolationMixin,
    SelectionMixin,
    dynamicProperty,
    reference
)
from fontParts.base import normalizers
from fontParts.base.compatibility import LayerCompatibilityReporter
from fontParts.base.color import Color
from fontParts.base.deprecated import DeprecatedLayer, RemovedLayer


class _BaseGlyphVendor(
                       BaseObject,
                       SelectionMixin,
                       ):

    """
    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 KeyError("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):
        """
        Insert **glyph** into the layer. ::

            >>> glyph = layer["A"] = otherGlyph

        This will not insert the glyph directly. Rather, a
        new glyph will be created and the data from **glyph**
        will be copied 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`.
        """
        name = normalizers.normalizeGlyphName(name)
        if name in self:
            del self[name]
        return self._insertGlyph(glyph, name=name)

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

            >>> del layer["A"]
        """
        name = normalizers.normalizeGlyphName(name)
        if name not in self:
            raise KeyError("No glyph named '%s'." % name)
        self._removeGlyph(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")

        This method is deprecated. :meth:`BaseFont.__delitem__` instead.
        """
        del self[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 method is deprecated. :meth:`BaseFont.__setitem__` instead.
        """
        if name is None:
            name = glyph.name
        self[name] = glyph

    def _insertGlyph(self, glyph, name, **kwargs):
        """
        This is the environment implementation of
        :meth:`BaseLayer.__setitem__` and :meth:`BaseFont.__setitem__`.
        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
        must not insert **glyph** directly. Instead the data from
        **glyph** should be copied to 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.
        """
        if glyph.name is None or name != glyph.name:
            glyph = glyph.copy()
            glyph.name = name
        dest = self.newGlyph(name, clear=kwargs.get("clear", True))
        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__


[docs]class BaseLayer(_BaseGlyphVendor, InterpolationMixin, DeprecatedLayer, RemovedLayer): def _reprContents(self): contents = [ "'%s'" % self.name, ] if self.color: contents.append("color=%r" % str(self.color)) return contents # ---- # Copy # ---- copyAttributes = ( "name", "color", "lib" )
[docs] 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): if self._font is not None: raise AssertionError("font for layer already set") 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) font = self.font if font is not None: existing = self.font.layerOrder if value in existing: raise ValueError("A layer with the name '%s' already exists." % value) self._set_name(value)
[docs] 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()
[docs] 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)
[docs] 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()
[docs] 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( "base_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
[docs] 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()
# tempLib tempLib = dynamicProperty( "base_tempLib", """ The layer's :class:`BaseLib` object. :: >>> layer.tempLib["org.robofab.hello"] "world" """ ) def _get_base_tempLib(self): lib = self._get_tempLib() lib.font = self return lib def _get_tempLib(self): """ This is the environment implementation of :attr:`BaseLayer.tempLib`. This must return an instance of a :class:`BaseLib` subclass. """ self.raiseNotImplementedError() # ----------------- # Global Operations # -----------------
[docs] 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()
[docs] def _round(self): """ This is the environment implementation of :meth:`BaseLayer.round`. Subclasses may override this method. """ for glyph in self: glyph.round()
[docs] 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()
[docs] def _autoUnicodes(self): """ This is the environment implementation of :meth:`BaseLayer.autoUnicodes`. Subclasses may override this method. """ for glyph in self: glyph.autoUnicodes()
# ------------- # Interpolation # -------------
[docs] 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)
[docs] 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
[docs] 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)
[docs] 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()