from __future__ import annotations
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union
from collections.abc import Callable
from fontTools.misc import transform
from fontTools.pens.pointInsidePen import PointInsidePen
from fontTools.pens.boundsPen import BoundsPen
from fontParts.base import normalizers
from fontParts.base.errors import FontPartsError
from fontParts.base.base import (
BaseObject,
TransformationMixin,
InterpolationMixin,
SelectionMixin,
IdentifierMixin,
dynamicProperty,
reference,
)
from fontParts.base.compatibility import ComponentCompatibilityReporter
from fontParts.base.deprecated import DeprecatedComponent, RemovedComponent
from fontParts.base.annotations import (
ScaleFactorPair,
ScaleFactor,
AffineTransformationLike,
AffineTransformation,
BoundingBox,
Coordinate,
CoordinateLike,
IntFloatType,
PenType,
PointPenType,
)
if TYPE_CHECKING:
from fontParts.base.glyph import BaseGlyph
from fontParts.base.layer import BaseLayer
from fontParts.base.font import BaseFont
[docs]
class BaseComponent(
BaseObject,
TransformationMixin,
InterpolationMixin,
SelectionMixin,
IdentifierMixin,
DeprecatedComponent,
RemovedComponent,
):
"""Represent the basis for a component object.
This object provides a reference to another glyph, allowing it to be
inserted as part of an outline.
"""
copyAttributes: tuple[str, str] = ("baseGlyph", "transformation")
def _reprContents(self) -> list[str]:
contents = [
f"baseGlyph='{self.baseGlyph}'",
f"offset='({self.offset[0]}, {self.offset[1]})'",
]
if self.glyph is not None:
contents.append("in glyph")
contents += self.glyph._reprContents()
return contents
# -------
# Parents
# -------
# Glyph
_glyph: Callable[[], BaseGlyph] | None = None
glyph: dynamicProperty = dynamicProperty(
"glyph",
"""Get or set the component's parent glyph object.
The value must be a :class:`BaseGlyph` instance or :obj:`None`.
:return: The :class:`BaseGlyph` instance containing the component
or :obj:`None`.
:raises AssertionError: If attempting to set the glyph when it
has already been set.
Example::
>>> glyph = component.glyph
""",
)
def _get_glyph(self) -> BaseGlyph | None:
if self._glyph is None:
return None
return self._glyph()
def _set_glyph(self, glyph: BaseGlyph | Callable[[], BaseGlyph] | None) -> None:
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 = dynamicProperty(
"layer",
"""Get the component's parent layer object.
This property is read-only.
:return: The :class:`BaseLayer` instance containing the component
or :obj:`None`.
Example::
>>> layer = component.layer
""",
)
def _get_layer(self) -> BaseLayer | None:
if self._glyph is None:
return None
return self.glyph.layer
# Font
font: dynamicProperty = dynamicProperty(
"font",
"""Get the component's parent font object.
This property is read-only.
:return: The :class:`BaseFont` instance containing the component
or :obj:`None`.
Example::
>>> font = component.font
""",
)
def _get_font(self) -> BaseFont | None:
if self._glyph is None:
return None
return self.glyph.font
# ----------
# Attributes
# ----------
# baseGlyph
baseGlyph: dynamicProperty = dynamicProperty(
"base_baseGlyph",
"""Get or set the name of the glyph referenced by the component.
The value must be a :class:`str`.
:return: A :class:`str` representing the name of the base glyph,
or :obj:`None` if the component does not belong to a layer.
:raise ValueError: If value is None when the component is part of a layer.
""",
)
def _get_base_baseGlyph(self) -> str | None:
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:
return value
if value is None:
raise ValueError(f"Value cannot be None when layer is '{self.layer}'.")
return normalizers.normalizeGlyphName(value)
def _set_base_baseGlyph(self, value: str) -> None:
value = normalizers.normalizeGlyphName(value)
self._set_baseGlyph(value)
[docs]
def _get_baseGlyph(self) -> str | None:
"""Get the name of the glyph referenced by the native component.
This is the environment implementation of the :attr:`BaseComponent.baseGlyph`
property getter.
:return: A :class:`str` representing the name of the base glyph,
or :obj:`None` if the component does not belong to a layer. The value
will be normalized with :func:`normalizers.normalizeGlyphName`.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def _set_baseGlyph(self, value: str) -> None:
"""Set the name of the glyph referenced by the native component.
This is the environment implementation of the :attr:`BaseComponent.baseGlyph`
property setter.
:param value: The name of the glyph to set as a :class:`str`. The value
will have been normalized
with :func:`normalizers.normalizeGlyphName`.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# transformation
transformation: dynamicProperty = dynamicProperty(
"base_transformation",
"""Get or set the component's transformation matrix.
The value must be a :ref:`type-transformation`.
:return: A :ref:`type-transformation` value representing the
transformation matrix of the component.
""",
)
def _get_base_transformation(self) -> AffineTransformation:
value = self._get_transformation()
value = normalizers.normalizeTransformationMatrix(value)
return value
def _set_base_transformation(self, value: AffineTransformationLike) -> None:
value = normalizers.normalizeTransformationMatrix(value)
self._set_transformation(value)
# offset
offset: dynamicProperty = dynamicProperty(
"base_offset",
"""Get or set the component's offset.
The value must be a :ref:`type-coordinate`.
:return: A :ref:`type-coordinate` representing the offset of the component.
""",
)
def _get_base_offset(self) -> Coordinate:
value = self._get_offset()
value = normalizers.normalizeTransformationOffset(value)
return value
def _set_base_offset(self, value: CoordinateLike) -> None:
value = normalizers.normalizeTransformationOffset(value)
self._set_offset(value)
[docs]
def _get_offset(self) -> Coordinate:
"""Get the native component's offset.
This is the environment implementation of the :attr:`BaseComponent.offset`
property getter.
:return: A :ref:`type-coordinate` representing the offset of the component.
The value will be normalized
with :func:`normalizers.normalizeTransformationOffset`.
.. note::
Subclasses may override this method.
"""
sx, sxy, syx, sy, ox, oy = self.transformation
return ox, oy
[docs]
def _set_offset(self, value: CoordinateLike) -> None:
"""Set the native component's offset.
This is the environment implementation of the :attr:`BaseComponent.offset`
property setter.
:param value: The offset to set as a :ref:`type-coordinate`. The value will
have been normalized with :func:`normalizers.normalizeTransformationOffset`.
.. note::
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 = dynamicProperty(
"base_scale",
"""Get or set the component's scale.
The value must be a :class:`list` or :class:`tuple` of two :class:`int`
or :class:`float` items representing the ``(x, y)`` scale of the component.
:return: A :class:`tuple` of two :class:`float` items representing the
``(x, y)`` scale of the component.
""",
)
def _get_base_scale(self) -> ScaleFactor:
value = self._get_scale()
value = normalizers.normalizeComponentScale(value)
return value
def _set_base_scale(self, value: ScaleFactorPair) -> None:
value = normalizers.normalizeComponentScale(value)
self._set_scale(value)
[docs]
def _get_scale(self) -> ScaleFactor:
"""Get the native component's scale.
This is the environment implementation of the :attr:`BaseComponent.scale`
property getter.
:return: A :class:`tuple` of two :class:`float` items representing the
``(x, y)`` scale of the component. The value will have been normalized
with :func:`normalizers.normalizeComponentScale`.
.. note::
Subclasses may override this method.
"""
sx, sxy, syx, sy, ox, oy = self.transformation
return sx, sy
[docs]
def _set_scale(self, value: ScaleFactorPair) -> None:
"""Set the native component's scale.
This is the environment implementation of the :attr:`BaseComponent.scale`
property setter.
:param value: The scale to set as a :class:`list` or :class:`tuple`
of :class:`int` or :class:`float` items representing the ``(x, y)``
scale of the component. The value will have been normalized
with :func:`normalizers.normalizeComponentScale`.
.. note::
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 = dynamicProperty(
"base_index",
"""Get or set the index of the contour.
The value must be an :class:`int`
:return: An :class:`int` representing the index of the component within
the ordered list of parent glyph's components, or :obj:`None` if the
component does not belong to a glyph.
:raise FontPartsError: If attempting to set the index while the
component does not belong to a glyph.
""",
)
def _get_base_index(self) -> int | None:
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: int) -> None:
glyph = self.glyph
if glyph is None:
raise FontPartsError("The component does not belong to a glyph.")
index = normalizers.normalizeIndex(value)
if index is None:
return
value = index
componentCount = len(glyph.components)
if value < 0:
value = -(value % componentCount)
if value >= componentCount:
value = componentCount
self._set_index(value)
[docs]
def _get_index(self) -> int | None:
"""Get the index of the native contour.
This is the environment implementation of the :attr:`BaseComponent.index`
property getter.
:return: An :class:`int` representing the index of the component within
the ordered list of parent glyph's components, or :obj:`None` if the
component does not belong to a glyph. The value will be normalized
with :func:`normalizers.normalizeIndex`.
.. note::
Subclasses may override this method.
"""
glyph = self.glyph
return glyph.components.index(self)
[docs]
def _set_index(self, value: int) -> None:
"""Set the index of the native contour.
This is the environment implementation of the :attr:`BaseComponent.index`
property setter.
:param value: The index to set as an :class:`int`. The value will have been
normalized with :func:`normalizers.normalizeIndex`.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# ----
# Pens
# ----
[docs]
def draw(self, pen: PenType) -> None:
"""Draw the component with the given pen.
:param pen: The :class:`fontTools.pens.basePen.AbstractPen` with which
to draw the componnet.
"""
self._draw(pen)
[docs]
def _draw(self, pen: PenType, **kwargs: Any) -> None:
r"""Draw the native component with the given pen.
This is the environment implementation of :meth:`BaseComponent.draw`.
:param pen: The :class:`fontTools.pens.basePen.AbstractPen` with which
to draw the componnet.
:param \**kwargs: Additional keyword arguments.
.. note::
Subclasses may override this method.
"""
from fontTools.ufoLib.pointPen import PointToSegmentPen
adapter = PointToSegmentPen(pen)
self.drawPoints(adapter)
[docs]
def drawPoints(self, pen: PointPenType) -> None:
"""Draw the component with the given point pen.
:param pen: The :class:`fontTools.pens.pointPen.AbstractPointPen` with
which to draw the componnet.
"""
self._drawPoints(pen)
[docs]
def _drawPoints(self, pen: PointPenType, **kwargs: Any) -> None:
r"""Draw the native component with the given point pen.
This is the environment implementation of :meth:`BaseComponent.draw`.
:param pen: The :class:`fontTools.pens.pointPen.AbstractPointPen` with
which to draw the componnet.
:param \**kwargs: Additional keyword arguments.
.. note::
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
# --------------
# -------------
# Normalization
# -------------
[docs]
def round(self) -> None:
"""Round the compnent's offset coordinates.
This applies to :attr:`offset`
"""
self._round()
[docs]
def _round(self) -> None:
"""Round the native compnent's offset coordinates.
This is the environment implementation of :meth:`BaseComponent.round`.
.. note::
Subclasses may override this method.
"""
x, y = self.offset
x = normalizers.normalizeVisualRounding(x)
y = normalizers.normalizeVisualRounding(y)
self.offset = (x, y)
[docs]
def decompose(self) -> None:
"""Decompose the component."""
glyph = self.glyph
if glyph is None:
raise FontPartsError("The component does not belong to a glyph.")
self._decompose()
[docs]
def _decompose(self) -> None:
"""Decompose the native component.
This is the environment implementation of :meth:`BaseComponent.decompose`.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# -------------
# Interpolation
# -------------
compatibilityReporterClass = ComponentCompatibilityReporter
def isCompatible(
self, other: BaseComponent, cls=None
) -> tuple[bool, ComponentCompatibilityReporter]:
"""Evaluate interpolation compatibility with another component.
:param other: The other :class:`BaseComponent` 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.ComponentCompatibilityReporter`
instance.
Example::
>>> compatible, report = self.isCompatible(otherComponent)
>>> compatible
True
>>> compatible
[Warning] Component: "A" + "B"
[Warning] Component: "A" has name A | "B" has name B
"""
return super().isCompatible(other, BaseComponent)
def _isCompatible(
self, other: BaseComponent, reporter: ComponentCompatibilityReporter
) -> None:
"""Evaluate interpolation compatibility with another native component.
This is the environment implementation of :meth:`BaseComponent.isCompatible`.
:param other: The other :class:`BaseComponent` instance to check
compatibility with.
:param reporter: An object used to report compatibility issues.
.. note::
Subclasses may override this method.
"""
component1 = self
component2 = other
# base glyphs
if component1.baseGlyph != component2.baseGlyph:
reporter.baseDifference = True
reporter.warning = True
# ------------
# Data Queries
# ------------
[docs]
def pointInside(self, point: CoordinateLike) -> bool:
"""Check if `point` lies inside the filled area of the component.
:param point: The point to check as a :ref:`type-coordinate`.
:return: :obj:`True` if `point` is inside the filled area of the
glyph, :obj:`False` otherwise.
Example::
>>> glyph.pointInside((40, 65))
True
"""
point = normalizers.normalizeCoordinateTuple(point)
return self._pointInside(point)
[docs]
def _pointInside(self, point: CoordinateLike) -> bool:
"""Check if `point` lies inside the filled area of the native component.
This is the environment implementation of :meth:`BaseComponent.pointInside`.
:param point: The point to check as a :ref:`type-coordinate`. The value will
have been normalized with :func:`normalizers.normalizeCoordinateTuple`.
:return: :class:`bool`.
.. note::
Subclasses may override this method.
"""
pen = PointInsidePen(glyphSet=self.layer, testPoint=point, evenOdd=False)
self.draw(pen)
return pen.getResult()
bounds: dynamicProperty = dynamicProperty(
"base_bounds",
"""Get the bounds of the component.
This property is read-only.
:return: A :class:`tuple` of four :class:`int` or :class:`float` values
in the form ``(x minimum, y minimum, x maximum, y maximum)``
representing the bounds of the component, or :obj:`None` if the
component is empty.
Example::
>>> component.bounds
(10, 30, 765, 643)
""",
)
def _get_base_bounds(self) -> BoundingBox:
value = self._get_bounds()
if value is not None:
value = normalizers.normalizeBoundingBox(value)
return value
[docs]
def _get_bounds(self) -> BoundingBox:
"""Get the bounds of the component.
This is the environment implementation of the :attr:`BaseComponent.bounds`
property getter.
:return: A :class:`tuple` of four :class:`int` or :class:`float` values
in the form ``(x minimum, y minimum, x maximum, y maximum)``
representing the bounds of the component, or :obj:`None` if the
component is empty. The value will be normalized
with :func:`normalizers.normalizeBoundingBox`.
.. note::
Subclasses may override this method.
"""
pen = BoundsPen(self.layer)
self.draw(pen)
return pen.bounds