Source code for fontParts.base.layer

# pylint: disable=C0103, C0302, C0114, W0613
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union
from collections.abc import Callable, Iterator
import collections

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
from fontParts.base.annotations import (
    InterpolationFactorPair,
    InterpolationFactorLike,
    RGBALike,
    RGBA,
    CharacterMappingType,
    CollectionType,
    ReverseComponentMappingType,
    IntFloatType,
)

if TYPE_CHECKING:
    from fontParts.base.font import BaseFont
    from fontParts.base.glyph import BaseGlyph
    from fontParts.base.lib import BaseLib


class _BaseGlyphVendor(BaseObject, SelectionMixin, ABC):
    """Provide common glyph interaction.

    This class provides common glyph interaction code to the
    to :class:`BaseFont` and :class`BaseLayer` classes.

    .. important::

        This class should not be directly subclassed.

    """

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

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

    def __len__(self) -> int:
        """Return the number of glyphs in the layer.

        :return: The number of :class:`BaseGlyph` instances in
            the layer as an :class:`int`.

        Example::

            >>> len(layer)
            256

        """
        return self._len()

    def _len(self, **kwargs: Any) -> int:
        r"""Return the number of glyphs in the native layer.

        This is the environment implementation of
        :meth:`BaseLayer.__len__` and :meth:`BaseFont.__len__`

        :param \**kwargs: Additional keyword arguments.
        :return: The number of :class:`BaseGlyph` subclass instances in
            the layer as an :class:`int`.

        .. note::

            Subclasses may override this method.

        """
        return len(self.keys())

    def __iter__(self) -> Iterator[BaseGlyph]:
        """Iterate through the glyphs in the layer.

        :return: An iterator over :class:`BaseGlyph` instances.

        Example::

            >>> for glyph in layer:
            ...     glyph.name
            "A"
            "B"
            "C"

        """
        return self._iter()

    def _iter(self, **kwargs: Any) -> Iterator[BaseGlyph]:
        """Iterate through the glyphs in the native layer.

        This is the environment implementation of
        :meth:`BaseLayer.__iter__` and :meth:`BaseFont.__iter__`.

        :return: An iterator over instances of a :class:`BaseGlyph` subclass.

        .. note::

            Subclasses may override this method.

        """
        for name in self.keys():
            yield self[name]

    def __getitem__(self, name: str) -> BaseGlyph:
        """Get the specified glyph from the layer.

        :param name: The name representing the glyph to retrieve.
        :return: a :class:`BaseGlyph` instance with the specified name.
        :raises KeyError: If no glyph with the given name exists in the layer.

        Example::

             >>> glyph = layer["A"]

        """
        name = normalizers.normalizeGlyphName(name)
        if name not in self:
            raise KeyError(f"No glyph named '{name}'.")
        glyph = self._getItem(name)
        self._setLayerInGlyph(glyph)
        return glyph

    def _getItem(self, name: str, **kwargs: Any) -> BaseGlyph:  # type: ignore[return]
        r"""Get the specified glyph from the native layer.

        This is the environment implementation of
        :meth:`BaseLayer.__getitem__` and :meth:`BaseFont.__getitem__`.

        :param name: The name representing the glyph to get. The value
            will have been normalized with :func:`normalizers.normalizeGlyphName`.
        :param \**kwargs: Additional keyword arguments.
        :return: an instance of a :class:`BaseGlyph` subclass with the
            specified name.
        :raises NotImplementedError: If the method has not been
            overridden by a subclass.

        .. important::

            Subclasses must override this method.

        """
        self.raiseNotImplementedError()

    def __setitem__(self, name: str, glyph: BaseGlyph) -> BaseGlyph:
        """Insert a specified glyph into the layer.

        This method will not insert a glyph directly, but rather create
        a new :class:`BaseGlyph` instance containing the data from
        `glyph`. The data inserted from `glyph` is the same data as
        documented in :meth:`BaseGlyph.copy`.

        :param name: The name to assign to the new layer after insertion.
        :param glyph: The :class:`BaseGlyph` instance to insert.
        :return: The newly inserted :class:`BaseGlyph` instance.
        :raises KeyError: If no glyph with the given name exists in the layer.

        Example::

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

        """
        name = normalizers.normalizeGlyphName(name)
        if name in self:
            del self[name]
        return self._insertGlyph(glyph, name=name)

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

        Example::

            >>> del layer["A"]

        """
        name = normalizers.normalizeGlyphName(name)
        if name not in self:
            raise KeyError(f"No glyph named '{name}'.")
        self._removeGlyph(name)

    def keys(self) -> tuple[str, ...]:
        """Get the names of all glyphs in the layer.

        This method returns an unordered :class:`tuple` of glyph names
        representing all the :class:`BaseGlyph` instances in the active
        layer. If called from a :class:`BaseFont` instance, it returns
        the glyphs from the default layer. If called from
        a :class:`BaseLayer` instance, it returns the glyphs from the
        current layer.

        :return: A :class:`tuple` of glyph names representing the glyphs
            in the current or default :class:`BaseLayer` instance.

        Example::

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

        """
        return self._keys()

    def _keys(self, **kwargs: Any) -> tuple[str, ...]:  # type: ignore[return]
        r"""Get the names of all glyphs in the native layer.

        This is the environment implementation of
        :meth:`BaseLayer.keys` and :meth:`BaseFont.keys`.

        :param \**kwargs: Additional keyword arguments.
        :return: An unordered :class:`tuple` of glyph names representing
            the glyphs in the current or default :class:`BaseLayer` instance.
        :raises NotImplementedError: If the method has not been
            overridden by a subclass.

         .. important::

            Subclasses must override this method.

        """
        self.raiseNotImplementedError()

    def __contains__(self, name: str) -> bool:
        """Check if the layer contains the specified glyph.

        This method checks whether a glyph with the given `name` exists
        in the layer. When called from a :class:`BaseFont` instance, it
        checks the default layer. When called from a :class:`BaseLayer`
        instance, it checks the current layer.

        :param name: The name of the glyph to check for.
        :return: :obj:`True` if the glyph exists in the layer,
            :obj:`False` otherwise.

        .. note::

            :meth:`has_key` is provided as an alias for this method for
            backward compatibility but may be deprecated in the future.
            It is advisable to use :meth:`__contains__` instead.

        Example::

            >>> "A" in layer
            True

        """
        name = normalizers.normalizeGlyphName(name)
        return self._contains(name)

    def _contains(self, name: str, **kwargs: Any) -> bool:
        r"""Test if the native layer contains the specified glyph.

        This is the environment implementation of
        :meth:`BaseLayer.__contains__` and :meth:`BaseFont.__contains__`.

        :param name: The name of the glyph to check. The value will have been
            normalized with :func:`normalizers.normalizeGlyphName`.
        :param \**kwargs: Additional keyword arguments.
        :return: :obj:`True` if the glyph exists in the layer,
            :obj:`False` otherwise.

        .. note::

            Subclasses may override this method.

        """
        return name in self.keys()

    def newGlyph(
        self, name: str, clear: bool = True, rename: bool = False
    ) -> BaseGlyph:
        """Create a new glyph in the layer.

        This method creates a new glyph with the given `name` in the layer.

        If no glyph with the specified `name` exists, a new glyph is created and
        returned.

        If a glyph with the specified `name` already exists:

        - If `clear` is :obj:`True`, the existing glyph is removed before creating and
          returning a new glyph with the same name.
        - If `clear` is :obj:`False` and `rename` is :obj:`False`, the existing glyph is
          returned unchanged.
        - If `clear` is :obj:`False` and `rename` is :obj:`True`, the existing glyph is
          renamed to a unique replacement name and a new glyph with the original `name`
          is created and returned.

        Replacement names are generated by appending a numeric suffix such as ``.1`` or
        ``.2`` to the original glyph name.

        When called from a :class:`BaseFont` instance, the glyph is created in the
        default layer. When called from a :class:`BaseLayer` instance, the glyph is
        created in the current layer.

        :param name: The name of the glyph to create.
        :param clear: Whether to remove an existing glyph with the same `name` before
            creating a new glyph. Defaults to :obj:`True`.
        :param rename: Whether to rename an existing glyph to a unique replacement name
            when `clear` is :obj:`False`. Defaults to :obj:`False`.
        :return: The newly created or existing :class:`BaseGlyph` instance.

        Example::

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

        """
        name = normalizers.normalizeGlyphName(name)
        if name not in self:
            glyph = self._newGlyph(name)
        elif clear:
            self.removeGlyph(name)
            glyph = self._newGlyph(name)
        elif rename:
            existingGlyph = self._getItem(name)
            newName = self._generateReplacementName(name)
            existingGlyph.name = newName
            glyph = self._newGlyph(name)
        else:
            glyph = self._getItem(name)
        self._setLayerInGlyph(glyph)
        return glyph

    def _generateReplacementName(self, name: str) -> str:
        if name not in self:
            return name
        i = 1
        while f"{name}.{i}" in self:
            i += 1
        return f"{name}.{i}"

    def _newGlyph(self, name: str, **kwargs: Any) -> BaseGlyph:  # type: ignore[return]
        r"""Create a new glyph in the native layer.

        This is the environment implementation of
        :meth:`BaseLayer.newGlyph` and :meth:`BaseFont.newGlyph`.

        :param name: The name of the glyph to create. The value will have been
            normalized with :func:`normalizers.normalizeGlyphName` and
            tested to make sure that it is unique to the layer.
        :param \**kwargs: Additional keyword arguments.
        :return: An instance of a :class:`BaseGlyph` subclass.
        :raises NotImplementedError: If the method has not been
            overridden by a subclass.

        .. important::

            Subclasses must override this method.

        """
        self.raiseNotImplementedError()

    def removeGlyph(self, name: str) -> None:
        """Remove the specified glyph from the layer.

        This method removes the glyph with the given `name` from the
        layer. When called from a :class:`BaseFont` instance, it
        removes the glyph from the default layer. When called from
        a :class:`BaseLayer` instance, it removes the glyph from the
        current layer.

        :param name: The name of the glyph to remove.

        Example::

            >>> layer.removeGlyph("A")

        """
        del self[name]

    def _removeGlyph(self, name: str, **kwargs: Any) -> None:
        r"""Remove the specified glyph from the native layer.

        This is the environment implementation of
        :meth:`BaseLayer.removeGlyph` and :meth:`BaseFont.removeGlyph`.

        :param name: The name of the glyph to remove. The value will
            have been normalized with :func:`normalizers.normalizeGlyphName`.
        :param \**kwargs: Additional keyword arguments.
        :raises NotImplementedError: If the method has not been
            overridden by a subclass.

        .. important::

            Subclasses must override this method.

        """
        self.raiseNotImplementedError()

    def insertGlyph(self, glyph: BaseGlyph, name: str | None = None) -> BaseGlyph:
        """Insert a specified glyph into the layer.

        .. deprecated::

            This method is deprecated. Use :meth:`BaseFont.__setitem__` instead.

        This method will not insert a glyph directly, but rather create
        a new :class:`BaseGlyph` instance containing the data from
        `glyph`. The data inserted from `glyph` is the same data as
        documented in :meth:`BaseGlyph.copy`.

        :param glyph: The :class:`BaseGlyph` instance to insert.
        :param name: The name to assign to the new layer after
            insertion. If value is :obj:`None`, the origninal name will
            be used. Defaults to :obj:`None`.
        :return: The newly inserted :class:`BaseGlyph` instance.

        Example::

            >>> glyph = font.insertGlyph(otherGlyph, name="glyph2")

        """
        if name is None:
            name = glyph.name
        self[name] = glyph
        return self[name]

    def _insertGlyph(self, glyph: BaseGlyph, name: str, **kwargs: Any) -> BaseGlyph:
        r"""Insert a specified glyph into the native layer.

        This is the environment implementation of
        :meth:`BaseLayer.__setitem__` and :meth:`BaseFont.__setitem__`.

        An environment must not insert `glyph` directly, but rather copy
        it's data to a new layer.

        :param glyph: A glyph object with the attributes necessary
            for copying as defined in :meth:`BaseGlyph.copy`
        :param name: The name to assign to the new glyph after
            insertion. The value will have been normalized
            with :func:`normalizers.normalizeGlyphName` and tested to
            make sure that it is unique to the layer.
        :param \**kwargs: Additional keyword arguments.
        :return: The newly inserted :class:`BaseLayer` subclass instance.

        .. note::

            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 = dynamicProperty(
        "base_selectedGlyphs",
        """Get or set the selected glyphs in the layer.

        The value must be a :class:`list` or :class:`tuple`
        of :class:`BaseGlyph` instances.

        :return: An unordered :class:`tuple` of currently selected
            :class:`BaseGlyph` instances.

        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) -> tuple[BaseGlyph, ...]:
        selected = tuple(
            normalizers.normalizeGlyph(glyph) for glyph in self._get_selectedGlyphs()
        )
        return selected

    def _get_selectedGlyphs(self) -> tuple[BaseGlyph, ...]:
        """Get the selected glyphs in the native layer.

        This is the environment implementation of
        the :attr:`BaseLayer.selectedGlyphs` property getter.

        :return: An unordered :class:`tuple` of selected :class:`BaseGlyph`
            subclass instances. Each value item will be normalized
            with :func:`normalizers.normalizeGlyph`.

        .. note::

            Subclasses may override this method.

        """
        return self._getSelectedSubObjects(self)

    def _set_base_selectedGlyphs(self, value: CollectionType[BaseGlyph]) -> None:
        normalized = [normalizers.normalizeGlyph(glyph) for glyph in value]
        self._set_selectedGlyphs(normalized)

    def _set_selectedGlyphs(self, value: CollectionType[BaseGlyph]) -> None:
        """Set the selected glyphs in the native layer.

        This is the environment implementation of
        the :attr:`BaseLayer.selectedGlyphs` property setter.

        :param value: A :class:`list` or :class:`tuple` of :class:`BaseGlyph`
            subclass instances to select. Each value item will have been normalized
            with :func:`normalizers.normalizeGlyph`.

        .. note::

            Subclasses may override this method.

        """
        return self._setSelectedSubObjects(self, value)

    selectedGlyphNames: dynamicProperty = dynamicProperty(
        "base_selectedGlyphNames",
        """Get or set the selected glyph names in the layer.

        The value must be a :class:`list` or :class:`tuple` of names
        representing :class:`BaseGlyph` instances.

        :return: An unordered :class:`tuple` of glyph names representing
            the currently selected :class:`BaseGlyph` instances.

        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) -> tuple[str, ...]:
        selected = tuple(
            normalizers.normalizeGlyphName(name)
            for name in self._get_selectedGlyphNames()
        )
        return selected

    def _get_selectedGlyphNames(self) -> tuple[str, ...]:
        """Get the selected glyph names in the layer.

        This is the environment implementation of
        the :attr:`BaseLayer.selectedGlyphNames` property getter.

        :return: An unordered :class:`tuple` of glyph names representing the
            currently selected :class:`BaseGlyph` subclass instances. Each value
            item will be normalized with :func:`normalizers.normalizeGlyphName`.

        .. note::

            Subclasses may override this method.

        """
        selected = tuple(glyph.name for glyph in self.selectedGlyphs)
        return selected

    def _set_base_selectedGlyphNames(self, value: CollectionType[str]) -> None:
        normalized = [normalizers.normalizeGlyphName(name) for name in value]
        self._set_selectedGlyphNames(normalized)

    def _set_selectedGlyphNames(self, value: CollectionType[str]) -> None:
        """Set the selected glyph names in the layer.

        This is the environment implementation of
        the :attr:`BaseLayer.selectedGlyphNames` property setter.

        :param value: A :class:`list` or :class:`tuple` of names representing
            the :class:`BaseGlyph` subclass instances to select. Each value item
            will have been normalized with :func:`normalizers.normalizeGlyphName`.

        .. note::

            Subclasses may override this method.

        """
        select = [self[name] for name in value]
        self.selectedGlyphs = select

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

    has_key: Callable[[_BaseGlyphVendor, str], bool] = __contains__

    # ----------------
    # Abstract Members
    # ----------------

    defaultLayer: dynamicProperty = dynamicProperty("base_defaultLayer")

    @abstractmethod
    def _get_base_defaultLayer(self) -> BaseLayer:
        pass

    @abstractmethod
    def _set_base_defaultLayer(self, layer: BaseLayer) -> None:
        pass

    @abstractmethod
    def _get_defaultLayer(self) -> BaseLayer:
        pass

    @abstractmethod
    def _set_defaultLayer(self, value: BaseLayer) -> None:
        pass


[docs] class BaseLayer(_BaseGlyphVendor, InterpolationMixin, DeprecatedLayer, RemovedLayer): """Represent the basis for a layer object. This object will almost always be created by retrieving it from a font object. It can exist at either the font or glyph level. See :ref:`layers`. """ def _reprContents(self) -> list[str]: contents: list[str] = [f"'{self.name}'"] if self.color: contents.append(f"color={self.color!r}") return contents # ---- # Copy # ---- copyAttributes: tuple[str, ...] = ("name", "color", "lib")
[docs] def copy(self) -> BaseLayer: """Copy data from the current layer into a new layer. This will copy: - :attr:`~BaseLayer.name` - :attr:`~BaseLayer.color` - :attr:`~BaseLayer.lib` - :meth:`glyphs<__iter__>` :return: A new :class:`BaseLayer` instance with the same attributes. Example:: >>> copiedLayer = layer.copy() """ return super().copy()
[docs] def copyData(self, source: BaseLayer) -> None: """Copy data from another layer instance. Refer to :meth:`BaseLayer.copy` for a list of values that will be copied. :param source: The source :class`BaseLayer` instance from which to copy data. Example:: >>> sourceFont = MyFont('path/to/source.ufo') >>> font.copyData(sourceFont) """ super().copyData(source) for name in source.keys(): glyph = self.newGlyph(name) glyph.copyData(source[name])
# ------- # Parents # ------- # Font _font: Callable[[], BaseFont] | None = None font: dynamicProperty = dynamicProperty( "font", """Get or set the layer's parent font object. The value must be a :class:`BaseFont` instance or :obj:`None`. :return: The :class:`BaseFont` instance containing the layer or :obj:`None`. :raises AssertionError: If attempting to set the font when it has already been set. Example:: >>> font = layer.font """, ) def _get_font(self) -> BaseFont | None: if self._font is None: return None return self._font() def _set_font(self, font: BaseFont | Callable[[], BaseFont] | None) -> None: 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 = dynamicProperty( "base_name", """Get or set the name of the layer. The value must be a :class:`str`. :return: A :class:`str` defining the name of the current layer or :obj:`None` if the layer is the default layer. :raises ValueError: If attempting to set the name to one that already exists in the font. Example:: >>> layer.name "foreground" >>> layer.name = "top" """, ) def _get_base_name(self) -> str | None: value = self._get_name() if value is not None: value = normalizers.normalizeLayerName(value) return value def _set_base_name(self, value: str) -> None: 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(f"A layer with the name '{value}' already exists.") self._set_name(value)
[docs] def _get_name(self) -> str | None: # type: ignore[return] """Get the name of the native layer. This is the environment implementation of the :attr:`BaseLayer.name` property getter. :return A :class:`str` defining the name of the current layer or :obj:`None` to indicate that the layer is the default. The value will be normalized with :func:`normalizers.normalizeLayerName`. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError()
[docs] def _set_name(self, value: str, **kwargs: Any) -> None: r"""Set the name of the native layer. This is the environment implementation of the :attr:`BaseLayer.name` property setter. :param value: The name to assign to the layer. The value must be unique to the font and will have been normalized with :func:`normalizers.normalizeLayerName` :param \**kwargs: Additional keyword arguments. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError()
# color color: dynamicProperty = dynamicProperty( "base_color", """Get or set the color of the layer. The value must be a :class:`tuple` of :class:`int` or :class:`float` numbers representing a :ref:`type-color`, or :obj:`None` to indicate that the layer does not have an assigned color. :return: A :class:`tuple` containing :class:`int` or :class:`float` values representing the color, or :obj:`None` if no color is assigned. Example:: >>> layer.color None >>> layer.color = (1, 0, 0, 0.5) """, ) def _get_base_color(self) -> Color | None: value = self._get_color() if value is not None: value = Color(value) return value def _set_base_color(self, value: RGBALike | None) -> None: if value is not None: value = normalizers.normalizeColor(value) self._set_color(value)
[docs] def _get_color(self) -> RGBA | None: # type: ignore[return] """Get the color of the layer. This is the environment implementation of the :attr:`BaseLayer.color` property getter. :return: The :ref:`type-color` assigned to the layer, or :obj:`None` to indicate that the layer does not have an assigned color. The value will be normalized with :func:`normalizers.normalizeColor`. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError()
[docs] def _set_color(self, value: RGBALike | None, **kwargs: Any) -> None: r"""Get or set the color of the layer. This is the environment implementation of the :attr:`BaseLayer.color` property setter. :param value: A :ref:`type-color` or :obj:`None` defining the color to assign to the layer. The value will have been normalized with :func:`normalizers.normalizeColor`. :param \**kwargs: Additional keyword arguments. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError()
# ----------- # Sub-Objects # ----------- # lib lib: dynamicProperty = dynamicProperty( "base_lib", """Get the layer's lib object. This property is read-only. :return: An instance of the :class:`BaseLib` class. Example:: >>> layer.lib["org.robofab.hello"] "world" """, ) def _get_base_lib(self) -> BaseLib: lib = self._get_lib() lib.font = self return lib
[docs] def _get_lib(self) -> BaseLib: # type: ignore[return] """Get the native layer's :class:`BaseLib` object. This is the environment implementation of the :attr:`BaseLayer.lib` property getter. :return: An instance of a :class:`BaseLib` subclass. :raises NotImplementedError: If the method has not been overridden by a subclass. """ self.raiseNotImplementedError()
# tempLib tempLib: dynamicProperty = dynamicProperty( "base_tempLib", """Get the layer's temporary lib object. This property is read-only. This property provides access to a temporary instance of the :class:`BaseLib` class, used for storing data that should not be persisted. It is similar to :attr:`BaseLayer.lib`, except that its contents will not be saved when calling the :meth:`BaseLayer.save` method. :return: A temporary instance of the :class:`BaseLib` class. Example:: >>> layer.tempLib["org.robofab.hello"] "world" """, ) def _get_base_tempLib(self) -> BaseLib: lib = self._get_tempLib() lib.font = self return lib def _get_tempLib(self) -> BaseLib: # type: ignore[return] """Get the layer's temporary lib object. This is the environment implementation of the :attr:`BaseLayer.lib` property setter. :return: A temporary instance of a :class:`BaseLib` subclass. :raises NotImplementedError: If the method has not been overridden by a subclass. .. important:: Subclasses must override this method. """ self.raiseNotImplementedError() # ----------------- # Global Operations # -----------------
[docs] def round(self) -> None: """Round all approriate layer data to integers. This is the equivalent of calling the :meth:`BaseGlyph.round` method on all glyphs in the layer. Example:: >>> layer.round() """ self._round()
[docs] def _round(self) -> None: """Round all approriate native layer data to integers. This is the environment implementation of :meth:`BaseLayer.round`. .. note:: Subclasses may override this method. """ for glyph in self: glyph.round()
[docs] def autoUnicodes(self) -> None: """Use heuristics to set Unicode values in all font glyphs. Environments will define their own heuristics for automatically determining values. Example:: >>> layer.autoUnicodes() """ self._autoUnicodes()
[docs] def _autoUnicodes(self) -> None: """Use heuristics to set Unicode values in all native font glyphs. This is the environment implementation of :meth:`BaseLayer.autoUnicodes`. .. note:: Subclasses may override this method. """ for glyph in self: glyph.autoUnicodes()
# ------------- # Interpolation # -------------
[docs] def interpolate( self, factor: InterpolationFactorLike, minLayer: BaseLayer, maxLayer: BaseLayer, round: bool = True, suppressError: bool = True, ) -> None: """Interpolate all possible data in the layer. The interpolation occurs on a 0 to 1.0 range between `minLayer` and `maxLayer`, using the specified `factor`. :param factor: The interpolation value as a single :class:`int` or :class:`float` or a :class:`tuple` of two :class:`int` or :class:`float` values representing the factors ``(x, y)``. :param minLayer: The :class:`BaseLayer` instance corresponding to the 0.0 position in the interpolation. :param maxLayer: The :class:`BaseLayer` instance corresponding to the 1.0 position in the interpolation. :param round: A :class:`bool` indicating whether the result should be rounded to integers. Defaults to :obj:`True`. :param suppressError: A :class:`bool` indicating whether to ignore incompatible data or raise an error when such incompatibilities are found. Defaults to :obj:`True`. :raises TypeError: If `minLayer` or `maxLayer` are not instances of :class:`BaseLayer`. Example:: >>> layer.interpolate(0.5, otherLayer1, otherLayer2) >>> layer.interpolate((0.5, 2.0), otherLayer1, otherLayer2, round=False) """ factor = normalizers.normalizeInterpolationFactor(factor) if not isinstance(minLayer, BaseLayer): raise TypeError( f"Interpolation to an instance of {self.__class__.__name__!r} can not be performed from an instance of {minLayer.__class__.__name__!r}." ) if not isinstance(maxLayer, BaseLayer): raise TypeError( f"Interpolation to an instance of {self.__class__.__name__!r} can not be performed from an instance of {maxLayer.__class__.__name__!r}." ) round = normalizers.normalizeBoolean(round) suppressError = normalizers.normalizeBoolean(suppressError) self._interpolate( factor, minLayer, maxLayer, round=round, suppressError=suppressError )
[docs] def _interpolate( self, factor: InterpolationFactorPair, minLayer: BaseLayer, maxLayer: BaseLayer, round: bool, suppressError: bool, ) -> None: """Interpolate all possible data in the native layer. This is the environment implementation of :meth:`BaseLayer.interpolate`. :param factor: The interpolation value as a single :class:`int` or :class:`float` or a :class:`tuple` of two :class:`int` or :class:`float` values representing the factors ``(x, y)``. :param minLayer: The :class:`BaseLayer` subclass instance corresponding to the 0.0 position in the interpolation. :param maxLayer: The :class:`BaseLayer` subclass instance corresponding to the 1.0 position in the interpolation. :param round: A :class:`bool` indicating whether the result should be rounded to integers. :param suppressError: A :class:`bool` indicating whether to ignore incompatible data or raise an error when such incompatibilities are found. :raises FontPartsError: If ``suppressError=False`` and the interpolation data is incompatible. .. note:: 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: BaseLayer, cls=None ) -> tuple[bool, LayerCompatibilityReporter]: """Evaluate interpolation compatibility with another layer. :param other: The other :class:`BaseLayer` instance to check compatibility with. :return: A :class:`tuple` where the first element is a :class:`bool` indicating compatibility, and the second element is a :class:`fontParts.base.compatibility.LayerCompatibilityReporter` instance. Example:: >>> compatible, report = self.isCompatible(otherLayer) >>> compatible False >>> report A - [Fatal] The glyphs do not contain the same number of contours. """ return super().isCompatible(other, BaseLayer)
[docs] def _isCompatible( self, other: BaseLayer, reporter: LayerCompatibilityReporter ) -> None: """Evaluate interpolation compatibility with another native layer. This is the environment implementation of :meth:`BaseFont.isCompatible`. :param other: The other :class:`BaseLayer` subclass instance to check compatibility with. :param reporter: An object used to report compatibility issues. .. note:: 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 # -------
[docs] def getReverseComponentMapping(self) -> ReverseComponentMappingType: """Get a reversed map of the layer's component references. This method creates a :class:`dict` mapping the name of each component base glyph in the font to a :class:`tuple` containing the composite glyph names that include the comoponent. All glyphs are loaded. :return: A :class:`dict` of component glyph names mapped to tuples of composite glyph names. Example:: >>> mapping = layer.getReverseComponentMapping() >>> mapping {'A': ('Aacute', 'Aring'), 'acute': ('Aacute',), 'ring': ('Aring',), ...} """ return self._getReverseComponentMapping()
def _getReverseComponentMapping(self) -> ReverseComponentMappingType: """Get a reversed map of the native layer's component references. This is the environment implementation of :meth:`BaseFont.getReverseComponentMapping`. .. note:: Subclasses may override this method. """ mapping = collections.defaultdict(list) for glyph in self: if not glyph.components: continue for component in glyph.components: baseGlyph = component.baseGlyph mapping[baseGlyph].append(glyph.name) return {k: tuple(v) for k, v in mapping.items()}
[docs] def getCharacterMapping(self) -> CharacterMappingType: """Get the layer's character mapping. This method creates a :class:`dict` mapping Unicode values to tuples of glyph names. Each Unicode value corresponds to one or more glyphs, and the glyph names represent these glyphs in the mapping. .. note:: One glyph can have multiple unicode values, and a unicode value can have multiple glyphs pointing to it. :return: A :class:`dict` mapping Unicode values to tuples of glyph names. """ return self._getCharacterMapping()
def _getCharacterMapping(self) -> CharacterMappingType: """Get the native layer's character mapping. This is the environment implementation of :meth:`BaseFont.getCharacterMapping`. .. note:: Subclasses may override this method. """ mapping = collections.defaultdict(list) for glyph in self: if not glyph.unicodes: continue for code in glyph.unicodes: mapping[code].append(glyph.name) return {k: tuple(v) for k, v in mapping.items()} # ------------------------- # Abstract Member Overrides # ------------------------- defaultLayer: dynamicProperty = dynamicProperty("base_defaultLayer") def _get_base_defaultLayer(self) -> BaseLayer: raise NotImplementedError("BaseLayer does not implement this method.") def _set_base_defaultLayer(self, layer: BaseLayer) -> None: raise NotImplementedError("BaseLayer does not implement this method.") def _get_defaultLayer(self) -> BaseLayer: raise NotImplementedError("BaseLayer does not implement this method.") def _set_defaultLayer(self, value: BaseLayer) -> None: raise NotImplementedError("BaseLayer does not implement this method.")