Source code for fontParts.base.kerning

# pylint: disable=C0103, C0114
from __future__ import annotations

from collections.abc import Callable, Iterator, MutableMapping
from typing import TYPE_CHECKING, Dict, List, Optional, TypeVar, Union

from fontParts.base import normalizers
from fontParts.base.annotations import (
    InterpolationFactorLike,
    InterpolationFactorPair,
    ScaleFactorLike,
    ScaleFactorPair,
    KerningPairLike,
    KerningPair,
    Coordinate,
    CoordinateLike,
    IntFloatType,
)
from fontParts.base.base import BaseDict, dynamicProperty, interpolate, reference
from fontParts.base.deprecated import DeprecatedKerning, RemovedKerning

if TYPE_CHECKING:
    from fontParts.base.base import BaseItems, BaseKeys, BaseValues
    from fontParts.base.font import BaseFont

BaseKerningType = TypeVar("BaseKerningType", bound="BaseKerning")


[docs] class BaseKerning(BaseDict, DeprecatedKerning, RemovedKerning): """Represent the basis for a kerning object. This object behaves like a Python :class:`dict` object. Most of the dictionary functionality comes from :class:`BaseDict`. Consult that object's documentation for the required environment implementation details. :cvar keyNormalizer: A function to normalize the key of the dictionary. Defaults to :func:`normalizers.normalizeKerningKey`. :cvar valueNormalizer: A function to normalize the value of the dictionary. Defaults to :func:`normalizers.normalizeKerningValue`. This object is normally created as part of a :class:`BaseFont`. An orphan :class:`BaseKerning` object instance can be created like this:: >>> groups = RKerning() """ keyNormalizer: Callable[[KerningPairLike], KerningPair] = ( normalizers.normalizeKerningKey ) valueNormalizer: Callable[[IntFloatType], IntFloatType] = ( normalizers.normalizeKerningValue ) def _reprContents(self) -> list[str]: contents = [] if self.font is not None: contents.append("for font") contents += self.font._reprContents() return contents # ------- # Parents # ------- # Font _font: Callable[[], BaseFont] | None = None font: dynamicProperty = dynamicProperty( "font", """Get or set the kerning's parent font object. The value must be a :class:`BaseFont` instance or :obj:`None`. :return: The :class:`BaseFont` instance containing the kerning or :obj:`None`. :raises AssertionError: If attempting to set the font when it has already been set and is not the same as the provided font. Example:: >>> font = kerning.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 and self._font() != font: raise AssertionError("font for kerning already set and is not same as font") if font is not None: font = reference(font) self._font = font # -------------- # Transformation # --------------
[docs] def scaleBy(self, factor: ScaleFactorLike) -> None: """Scale all kerning values by the specified factor. :param factor: The factor by which to scale the kerning. The value may be a single :class:`int` or :class:`float` or a :class:`tuple` or :class`list` of two :class:`int` or :class:`float` values representing the factors ``(x, y)``. In the latter case, the first value is used to scale the kerning values. Example:: >>> myKerning.scaleBy(2) >>> myKerning.scaleBy((2, 3)) """ factor = normalizers.normalizeTransformationScale(factor) self._scale(factor)
[docs] def _scale(self, factor: ScaleFactorPair) -> None: """Scale all native kerning values by the specified factor. This is the environment implementation of :meth:`BaseKerning.scaleBy`. :param factor: The factor by which to scale the kerning as a :class:`tuple` of two :class:`int` or :class:`float` values representing the factors ``(x, y)``. The first value is used to scale the kerning values. .. note:: Subclasses may override this method. """ horizontalFactor = factor[0] for k, v in self.items(): v *= horizontalFactor self[k] = v
# ------------- # Normalization # -------------
[docs] def round(self, multiple: int = 1) -> None: """Round the kerning values to the specified increments. :param multiple: The increment to which the kerning values should be rounded as an :class:`int`. Defaults to ``1``. Example:: >>> myKerning.round(2) """ if not isinstance(multiple, int): raise TypeError( f"The round multiple must be an int not {multiple.__class__.__name__}." ) self._round(multiple)
[docs] def _round(self, multiple: int) -> None: """Round the native kerning values to the specified increments. This is the environment implementation of :meth:`BaseKerning.round`. :param multiple: The increment to which the kerning values should be rounded as an :class:`int`. .. note:: Subclasses may override this method. """ for pair, value in self.items(): value = ( int(normalizers.normalizeVisualRounding(value / float(multiple))) * multiple ) self[pair] = value
# ------------- # Interpolation # -------------
[docs] def interpolate( self, factor: InterpolationFactorLike, minKerning: BaseKerningType, maxKerning: BaseKerningType, round: bool = True, suppressError: bool = True, ) -> None: """Interpolate all kerning pairs in the font. The kerning data will be replaced by the interpolated kerning. :param factor: The interpolation value as a single :class:`int` or :class:`float` or a :class:`list` or :class:`tuple` of two :class:`int` or :class:`float` values representing the factors ``(x, y)``. :param minKerning: The :class:`BaseKerning` instance corresponding to the 0.0 position in the interpolation. :param maxKerning: The :class:`BaseKerning` 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 `minGlyph` or `maxGlyph` are not instances of :class:`BaseKerning`. Example:: >>> myKerning.interpolate(kerningOne, kerningTwo) """ factor = normalizers.normalizeInterpolationFactor(factor) if not isinstance(minKerning, BaseKerning): raise TypeError( f"Interpolation to an instance of {self.__class__.__name__!r} can not be performed from an instance of {minKerning.__class__.__name__!r}." ) if not isinstance(maxKerning, BaseKerning): raise TypeError( f"Interpolation to an instance of {self.__class__.__name__!r} can not be performed from an instance of {maxKerning.__class__.__name__!r}." ) round = normalizers.normalizeBoolean(round) suppressError = normalizers.normalizeBoolean(suppressError) self._interpolate( factor, minKerning, maxKerning, round=round, suppressError=suppressError )
[docs] def _interpolate( self, factor: InterpolationFactorPair, minKerning: BaseKerning, maxKerning: BaseKerning, round: bool, suppressError: bool, ) -> None: """Interpolate all kerning pairs in the native font. The kerning data will be replaced by the interpolated kerning. :param factor: The interpolation value as a single :class:`int` or :class:`float` or a :class:`list` or :class:`tuple` of two :class:`int` or :class:`float` values representing the factors ``(x, y)``. :param minKerning: The :class:`BaseKerning` subclass instance corresponding to the 0.0 position in the interpolation. :param maxKerning: The :class:`BaseKerning` 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. .. note:: Subclasses may override this method. """ from fontMath import MathKerning from fontMath.mathFunctions import setRoundIntegerFunction setRoundIntegerFunction(normalizers.normalizeVisualRounding) kerningGroupCompatibility = self._testKerningGroupCompatibility( minKerning, maxKerning, suppressError=suppressError ) if not kerningGroupCompatibility: self.clear() else: minMathKerning = MathKerning( kerning=minKerning, groups=minKerning.font.groups ) maxMathKerning = MathKerning( kerning=maxKerning, groups=maxKerning.font.groups ) result = interpolate(minMathKerning, maxMathKerning, factor) if round: result.round() self.clear() result.extractKerning(self.font)
@staticmethod def _testKerningGroupCompatibility( minKerning: BaseKerning, maxKerning: BaseKerning, suppressError: bool = False ) -> bool: minGroups = minKerning.font.groups maxGroups = maxKerning.font.groups match = True while match: for _, sideAttr in ( ("side 1", "side1KerningGroups"), ("side 2", "side2KerningGroups"), ): minSideGroups = getattr(minGroups, sideAttr) maxSideGroups = getattr(maxGroups, sideAttr) if minSideGroups.keys() != maxSideGroups.keys(): match = False else: for name in minSideGroups.keys(): minGroup = minSideGroups[name] maxGroup = maxSideGroups[name] if set(minGroup) != set(maxGroup): match = False break if not match and not suppressError: raise ValueError("The kerning groups must be exactly the same.") return match # --------------------- # RoboFab Compatibility # --------------------- def remove(self, pair: KerningPairLike) -> None: """Remove the specified pair from the Kerning. :param pair: The pair to remove as a :class:`tuple` of two :class:`str` values. .. note:: This is a backwards compatibility method. Example:: >>> myKerning.remove(("A", "V")) """ del self[pair] def asDict(self, returnIntegers: bool = True) -> dict[KerningPair, IntFloatType]: """Return the kerning as a dictionary. :return A :class:`dict` reflecting the contents of the kerning. .. note:: This is a backwards compatibility method. Example:: >>> font.lib.asDict() """ return { k: (v if not returnIntegers else normalizers.normalizeVisualRounding(v)) for k, v in self.items() } # ------------------- # Inherited Functions # -------------------
[docs] def __contains__(self, pair: KerningPairLike) -> bool: """Check if the given pair exists in the kerning. :param pair: The kerning pair to check for existence as a :class:`tuple` of two :class:`str` values. :return: :obj:`True` if the `groupName` exists in the groups, :obj:`False` otherwise. Example:: >>> ("A", "V") in font.kerning True """ return super().__contains__(pair)
[docs] def __delitem__(self, pair: KerningPairLike) -> None: """Remove the given pair from the kerning. :param pair: The pair to remove as a :class:`tuple` of two :class:`str` values. Example:: >>> del font.kerning[("A","V")] """ super().__delitem__(pair)
[docs] def __getitem__(self, pair: KerningPairLike) -> IntFloatType: """Get the value associated with the given kerning pair. :param pair: The pair to remove as a :class:`tuple` of two :class:`str` values. :return: The kerning value as an :class:`int` or :class:`float`. Example:: >>> font.kerning[("A", "V")] -15 .. note:: Any changes to the returned kerning value will not be reflected in it's :class:`BaseKerning` instance. To make changes to this value, do the following:: >>> value = font.kerning[("A", "V")] >>> value += 10 >>> font.kerning[("A", "V")] = value """ return super().__getitem__(pair)
[docs] def __iter__(self) -> Iterator[KerningPair]: """Return an iterator over the pairs in the kerning. The iteration order is not fixed. :return: An :class:`Iterator` over :class:`tuple` instances containing two :class:`str` values. Example:: >>> for pair in font.kerning: >>> print pair ("A", "Y") ("A", "V") ("A", "W") """ return super().__iter__()
[docs] def __len__(self) -> int: """Return the number of pairs in the kerning. :return: An :class:`int` representing the number of pairs in the kerning. Example:: >>> len(font.kerning) 5 """ return super().__len__()
[docs] def __setitem__(self, pair: KerningPairLike, value: IntFloatType) -> None: """Set the value for the given kerning pair. :param pair: The pair to set as a :class:`tuple` of two :class:`str` values. :param value: The value to set as an :class:`int` or :class:`float`. Example:: >>> font.kerning[("A", "V")] = -20 >>> font.kerning[("A", "W")] = -10.5 """ super().__setitem__(pair, value)
[docs] def clear(self) -> None: """Remove all information from kerning. This will reset the :class:`BaseKerning` instance to an empty dictionary. Example:: >>> font.kerning.clear() """ super().clear()
[docs] def get( self, pair: KerningPairLike, default: IntFloatType | None = None ) -> IntFloatType | None: """Get the value for the given kerning pair. If the given `pair` is not found, The specified `default` will be returned. :param pair: The pair to get as a :class:`tuple` of two :class:`str` values. :param default: The optional default value to return if the `pair` is not found. :return: A :class:`tuple` of two :class:`str` values representing the value for the given `pair`, or the `default` value if the `pair` is not found. Example:: >>> font.kerning.get(("A", "V")) -25 .. note:: Any changes to the returned kerning value will not be reflected in it's :class:`BaseKerning` instance. To make changes to this value, do the following:: >>> value = font.kerning[("A", "V")] >>> value += 10 >>> font.kerning[("A", "V")] = value """ return super().get(pair, default)
[docs] def find( self, pair: KerningPairLike, default: IntFloatType | None = None ) -> IntFloatType | None: """Get the value for the given explicit or implicit kerning pair. This method will return the value for the given `pair`, even if it only exists implicitly (one or both sides may be members of a kerning group). If the `pair` is not found, the specified `default` will be returned. :param pair: The pair to get as a :class:`tuple` of two :class:`str` values. :param default: The optional default value to return if the `pair` is not found. :return: A :class:`tuple` of two :class:`str` values representing the value for the given `pair`, or the `default` value if the `pair` is not found. Example:: >>> font.kerning.find(("A", "V")) -25 """ pair = normalizers.normalizeKerningKey(pair) value = self._find(pair, default) if value and value != default: value = normalizers.normalizeKerningValue(value) return value
def _find( self, pair: KerningPairLike, default: IntFloatType | None = None ) -> IntFloatType | None: """Get the value for the given explicit or implicit native kerning pair. This is the environment implementation of :attr:`BaseKerning.find`. :param pair: The pair to get as a :class:`tuple` of two :class:`str` values. :param default: The optional default value to return if the `pair` is not found. :return: A :class:`tuple` of two :class:`str` values representing the value for the given `pair`, or the `default` value if the `pair` is not found. .. note:: Subclasses may override this method. """ from fontTools.ufoLib.kerning import lookupKerningValue font = self.font groups = font.groups return lookupKerningValue(pair, self, groups, fallback=default)
[docs] def items(self) -> BaseItems[KerningPair, IntFloatType]: """Return the kerning's items. Each item is represented as a :class:`tuple` of key-value pairs, where: - `key` is a :class:`tuple` of two :class:`str` values. - `value` is an :class:`int` or a :class:`float`. :return: A :ref:`type-view` of the kerning's ``(key, value)`` pairs. Example:: >>> font.kerning.items() BaseKerning_items([(("A", "V"), -30), (("A", "W"), -10)]) """ return super().items()
[docs] def keys(self) -> BaseKeys[KerningPair]: """Return the kerning's pairs (keys). :return: A :ref:`type-view` of the kerning's pairs as :class:`tuple` instances of two :class:`str` values. Example:: >>> font.kerning.keys() BaseKerning_keys([("A", "Y"), ("A", "V"), ("A", "W")]) """ return super().keys()
[docs] def values(self) -> BaseValues[IntFloatType]: """Return the kerning's values. :return: A :ref:`type-view` of :class:`int` or :class:`float` values. Example:: >>> font.kerning.values() BaseKerning_values([-20, -15, 5, 3.5]) """ return super().values()
[docs] def pop( self, pair: KerningPairLike, default: IntFloatType | None = None ) -> IntFloatType | None: """Remove the specified kerning pair and return its associated value. If the `pair` does not exist, the `default` value is returned. :param pair: The pair to remove as a :class:`tuple` of two :class:`str` values. :param default: The optional default value to return if the `pair` is not found`. The value must be an :class:`int`, a :class:`float` or :obj:`None`. Defaults to :obj:`None`. :return: The value for the given `pair` as an :class:`int` or :class:`float`, or the `default` value if the `pair` is not found. Example:: >>> font.kerning.pop(("A", "V")) -20 >>> font.kerning.pop(("A", "W")) -10.5 """ return super().pop(pair, default)
[docs] def update(self, otherKerning: MutableMapping[KerningPair, IntFloatType]) -> None: """Update the current kerning with key-value pairs from another. For each pair in `otherKerning`: - If the pair exists in the current kerning, its value is replaced with the value from `otherKerning`. - If the pair does not exist in the current kerning, it is added. Pairs that exist in the current kerning but are not in `otherLib` remain unchanged. :param otherKerning: A :class:`MutableMapping` of key-value pairs to update the current lib with. Keys must be a :class:`tuple` of two :class:`str` values. Values must be an :class:`int` or a :class:`float`. Example:: >>> font.kerning.update(newKerning) """ super().update(otherKerning)