# pylint: disable=C0103, C0302, C0114, W0613
from __future__ import annotations
from typing import TYPE_CHECKING, Any, TypeVar
from collections.abc import Iterator
from itertools import zip_longest
from collections import Counter
import os
from copy import deepcopy
from fontMath import MathGlyph
from fontMath.mathFunctions import setRoundIntegerFunction
from fontTools.pens.pointInsidePen import PointInsidePen
from fontTools.pens.areaPen import AreaPen
from fontTools.pens.boundsPen import BoundsPen
from fontParts.base.errors import FontPartsError
from fontParts.base.base import (
BaseObject,
TransformationMixin,
InterpolationMixin,
SelectionMixin,
dynamicProperty,
interpolate,
FuzzyNumber,
)
from fontParts.base import normalizers
from fontParts.base.compatibility import GlyphCompatibilityReporter
from fontParts.base.color import Color
from fontParts.base.deprecated import DeprecatedGlyph, RemovedGlyph
from fontParts.base.annotations import (
InterpolationFactorLike,
InterpolationFactorPair,
ScaleFactorLike,
AffineTransformationLike,
BoundingBox,
Coordinate,
CoordinateLike,
RGBA,
RGBALike,
CollectionType,
IntFloatType,
DiffType,
PenType,
PointPenType,
)
if TYPE_CHECKING:
from fontParts.base.font import BaseFont
from fontParts.base.lib import BaseLib
from fontParts.base.layer import BaseLayer
from fontParts.base.guideline import BaseGuideline
from fontParts.base.contour import BaseContour
from fontParts.base.component import BaseComponent
from fontParts.base.anchor import BaseAnchor
from fontParts.base.image import BaseImage
TempContourListType = list[
tuple[int, int, IntFloatType, IntFloatType, IntFloatType, BaseContour]
]
ContourListType = list[
tuple[int, int, FuzzyNumber, FuzzyNumber, IntFloatType, BaseContour]
]
[docs]
class BaseGlyph(
BaseObject,
TransformationMixin,
InterpolationMixin,
SelectionMixin,
DeprecatedGlyph,
RemovedGlyph,
):
"""Represent the basis for a glyph object.
This object will almost always be created by retrieving it from a
font object.
"""
copyAttributes: tuple[str, ...] = (
"name",
"unicodes",
"width",
"height",
"note",
"markColor",
"lib",
)
def _reprContents(self) -> list[str]:
contents: list[str] = [f"'{self.name}'"]
if self.layer is not None:
contents.append(f"('{self.layer.name}')")
return contents
[docs]
def copy(self: BaseGlyph) -> BaseGlyph:
"""Copy data from the current glyph into a new glyph.
This new glyph object will not belong to a font.
This will copy:
- :attr:`name`
- :attr:`unicodes`
- :attr:`width`
- :attr:`height`
- :attr:`note`
- :attr:`markColor`
- :attr:`lib`
- :attr:`contours`
- :attr:`components`
- :attr:`anchors`
- :attr:`guidelines`
- :attr:`image`
:return: A new :class:`BaseGlyph` instance with the same attributes.
Example::
>>> copiedGlyph = glyph.copy()
"""
return super().copy()
def copyData(self: BaseGlyph, source: BaseGlyph) -> None:
"""Copy data from another glyph instance.
Refer to :meth:`BaseGlyph.copy` for a list of values that will
be copied.
:param source: The source :class:`BaseGlyph` instance from which
to copy data.
Example::
>>> glyph.copyData(sourceGlyph)
"""
super().copyData(source)
for contour in source.contours:
self.appendContour(contour)
for component in source.components:
self.appendComponent(component=component)
for anchor in source.anchors:
self.appendAnchor(anchor=anchor)
for guideline in source.guidelines:
self.appendGuideline(guideline=guideline)
sourceImage = source.image
if sourceImage.data is not None:
selfImage = self.addImage(data=sourceImage.data)
selfImage.transformation = sourceImage.transformation
selfImage.color = sourceImage.color
# -------
# Parents
# -------
# Layer
_layer: BaseLayer | None = None
layer: dynamicProperty = dynamicProperty(
"layer",
"""Get or set the glyph's parent layer object.
The value must be a :class:`BaseLayer` instance or :obj:`None`.
:return: The :class:`BaseLayer` instance containing the glyph
or :obj:`None`.
Example::
>>> layer = glyph.layer
""",
)
def _get_layer(self) -> BaseLayer | None:
if self._layer is None:
return None
return self._layer
def _set_layer(self, layer: BaseLayer | None) -> None:
self._layer = layer
# Font
font: dynamicProperty = dynamicProperty(
"font",
"""Get the glyph's parent font object.
This property is read-only.
:return: The :class:`BaseFont` instance containing the glyph
or :obj:`None`.
Example::
>>> font = glyph.font
""",
)
def _get_font(self) -> BaseFont | None:
if self._layer is None:
return None
return self.layer.font
# --------------
# Identification
# --------------
# Name
name: dynamicProperty = dynamicProperty(
"base_name",
"""Get or set the name of the glyph.
The value must be a :class:`str`.
:return: A :class:`str` defining the name of the glyph.
:raises ValueError: If attempting to set the name to one that
already exists in the layer.
Example::
>>> glyph.name
"A"
>>> glyph.name = "A.alt"
""",
)
def _get_base_name(self) -> str:
value = self._get_name()
if value is not None:
value = normalizers.normalizeGlyphName(value)
return value
def _set_base_name(self, value: str) -> None:
if value == self.name:
return
value = normalizers.normalizeGlyphName(value)
layer = self.layer
if layer is not None and value in layer:
raise ValueError(f"A glyph with the name '{value}' already exists.")
self._set_name(value)
[docs]
def _get_name(self) -> str: # type: ignore[return]
"""Get the name of the native glyph.
This is the environment implementation of the :attr:`BaseGlyph.name`
property getter.
:return A :class:`str` defining the name of the glyph. The value
will be normalized with :func:`normalizers.normalizeLayerName`.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def _set_name(self, value: str) -> None:
"""Set the name of the native glyph.
This is the environment implementation of the :attr:`BaseGlyph.name`
property setter.
:param value: The name to assign to the glyph as a :class:`str`. The value
will have been normalized with :func:`normalizers.normalizeGlyphName`
and must be unique to the layer.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# Unicodes
unicodes: dynamicProperty = dynamicProperty(
"base_unicodes",
"""Get or set the glyph's Unicode values.
The value must be a :class:`list` or :class:`tuple` of :class:`int` or
hexadecimal :class:`str` values, ordered from most to least important.
:return: A :class:`tuple` of :class:`int` values representing the
glyphs Unicode values in order from most to least important.
Example::
>>> glyph.unicodes
(65,)
>>> glyph.unicodes = [65, 66]
>>> glyph.unicodes = []
""",
)
def _get_base_unicodes(self) -> tuple[int, ...]:
value = self._get_unicodes()
value = normalizers.normalizeGlyphUnicodes(value)
return value
def _set_base_unicodes(self, value: CollectionType[int]) -> None:
value = tuple(value)
value = normalizers.normalizeGlyphUnicodes(value)
self._set_unicodes(value)
[docs]
def _get_unicodes(self) -> CollectionType[int]: # type: ignore[return]
"""Get the Unicode values assigned to the glyph.
This is the environment implementation of
the :attr:`BaseGlyph.unicodes` property getter.
:return: A :class:`tuple` of :class:`int` values representing
the glyphs Unicode values in order from most to least important.
The value will be normalized
with :func:`normalizers.normalizeGlyphUnicodes(value)`.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def _set_unicodes(self, value: CollectionType[int]) -> None:
"""Assign Unicode values to the glyph.
This is the environment implementation of
the :attr:`BaseGlyph.unicodes` property setter.
:param value: A :class:`list` or :class:`tuple` of :class:`int`
values in order from most to least important. The value will have
been normalized with :func:`normalizers.normalizeGlyphUnicodes(value)`.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
unicode: dynamicProperty = dynamicProperty(
"base_unicode",
"""Get or set the glyph's primary Unicode value.
This property is equivalent to ``glyph.unicodes[0]`` and will reset
:attr:`BaseGlyph.unicodes` to a :class:`tuple` containing that value,
or to an empty :class:`tuple` if value is :obj:`None`.
The value must be an :class:`int`, a hexadecimal :class:`str` or :obj:`None`.
:return: An :class:`int` representing the glyphs primary Unicode
value or :obj:`None`.
Example::
>>> glyph.unicode
65
>>> glyph.unicode = None
None
Interaction with the :attr:`BaseGlyph.unicodes` property::
>>> glyph.unicodes
(65, 67)
>>> glyph.unicode = 65
>>> glyph.unicodes
(65,)
>>> glyph.unicode = None
>>> glyph.unicodes
()
""",
)
def _get_base_unicode(self) -> int | None:
value = self._get_unicode()
if value is not None:
value = normalizers.normalizeGlyphUnicode(value)
return value
def _set_base_unicode(self, value: int | None) -> None:
if value is not None:
value = normalizers.normalizeGlyphUnicode(value)
self._set_unicode(value)
else:
self._set_unicodes(())
[docs]
def _get_unicode(self) -> int | None:
"""Get the primary Unicode value assigned to the native glyph.
This is the environment implementation of
the :attr:`BaseGlyph.unicode` property getter.
:return: An :class:`int` representing the glyphs primary Unicode
value or :obj:`None`.
.. note::
Subclasses may override this method.
"""
values = self.unicodes
if values:
return values[0]
return None
[docs]
def _set_unicode(self, value: int | None) -> None:
"""Assign the primary Unicode value to the native glyph.
This is the environment implementation of
the :attr:`BaseGlyph.unicode` property setter.
:param value: The primary Unicode value to assign as an :class:`int`
or :obj:`None`.
.. note::
Subclasses may override this method.
"""
if value is None:
self.unicodes = []
else:
self.unicodes = [value]
[docs]
def autoUnicodes(self) -> None:
"""Use heuristics to set the Unicode values in the glyph.
Environments will define their own heuristics for
automatically determining values.
Example::
>>> glyph.autoUnicodes()
"""
self._autoUnicodes()
[docs]
def _autoUnicodes(self) -> None:
"""Use heuristics to set the Unicode values in the native glyph.
This is the environment implementation of :meth:`BaseGlyph.autoUnicodes`.
:return: Description of :obj:`None`.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# -------
# Metrics
# -------
# horizontal
width: dynamicProperty = dynamicProperty(
"base_width",
"""Get or set the width of the glyph.
The value must be an :class:`int` or a :class:`float`.
:return: An :class:`int` or a :class:`float` representing
the width of the glyph.
Example::
>>> glyph.width
500
>>> glyph.width = 200
""",
)
def _get_base_width(self) -> IntFloatType:
value = self._get_width()
value = normalizers.normalizeGlyphWidth(value)
return value
def _set_base_width(self, value: IntFloatType) -> None:
value = normalizers.normalizeGlyphWidth(value)
self._set_width(value)
[docs]
def _get_width(self) -> IntFloatType: # type: ignore[return]
"""Get the width of the native glyph.
This is the environment implementation of
the :attr:`BaseGlyph.width` property getter.
:return: An :class:`int` or a :class:`float` representing
the width of the glyph.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def _set_width(self, value: IntFloatType) -> None:
"""Set the width of the native glyph.
This is the environment implementation of
the :attr:`BaseGlyph.width` property setter.
:param value: The glyph width as an :class:`int` or
a :class:`float`.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
leftMargin: dynamicProperty = dynamicProperty(
"base_leftMargin",
"""Get or set the glyph's left margin.
The value must be either an :class:`int` or a :class:`float`.
:return: The left glyph margin as an :class:`int` or a :class:`float`,
or :obj:`None` if the glyph has no outlines.
Example::
>>> glyph.leftMargin
35
>>> glyph.leftMargin = 45
""",
)
def _get_base_leftMargin(self) -> IntFloatType | None:
value = self._get_leftMargin()
value = normalizers.normalizeGlyphLeftMargin(value)
return value
def _set_base_leftMargin(self, value: IntFloatType) -> None:
normalizedValue = normalizers.normalizeGlyphLeftMargin(value)
# Avoid mypy conflict with normalizeGlyphLeftMargin -> Optional[IntFloat]
if normalizedValue is None:
raise TypeError("The value for leftMargin cannot be None.")
self._set_leftMargin(normalizedValue)
[docs]
def _get_leftMargin(self) -> IntFloatType | None:
"""Get the native glyph's left margin.
This is the environment implementation of
the :attr:`BaseGlyph.leftMargin` property getter.
:return: The left glyph margin as an :class:`int` or a :class:`float`,
or :obj:`None` if the glyph has no outlines.
.. note::
Subclasses may override this method.
"""
bounds = self.bounds
if bounds is None:
return None
xMin, yMin, xMax, yMax = bounds
return xMin
[docs]
def _set_leftMargin(self, value: IntFloatType) -> None:
"""Set the native glyph's left margin.
This is the environment implementation of
the :attr:`BaseGlyph.leftMargin` property setter.
:param value: The left glyph margin to set as an :class:`int` or
a :class:`float`.
.. note::
Subclasses may override this method.
"""
diff = value - self.leftMargin
self.moveBy((diff, 0))
self.width += diff
rightMargin: dynamicProperty = dynamicProperty(
"base_rightMargin",
"""Get or set the glyph's right margin.
The value must be either an :class:`int` or a :class:`float`.
:return: The right glyph margin as an :class:`int` or a :class:`float`,
or :obj:`None` if the glyph has no outlines.
Example::
>>> glyph.rightMargin
35
>>> glyph.rightMargin = 45
""",
)
def _get_base_rightMargin(self) -> IntFloatType | None:
value = self._get_rightMargin()
value = normalizers.normalizeGlyphRightMargin(value)
return value
def _set_base_rightMargin(self, value: IntFloatType) -> None:
normalizedValue = normalizers.normalizeGlyphRightMargin(value)
# Avoid mypy conflict with normalizeGlyphRightMargin -> Optional[IntFloat]
if normalizedValue is None:
raise TypeError("The value for rightMargin cannot be None.")
self._set_rightMargin(normalizedValue)
[docs]
def _get_rightMargin(self) -> IntFloatType | None:
"""Get the native glyph's right margin.
This is the environment implementation of
the :attr:`BaseGlyph.rightMargin` property getter.
:return: The right glyph margin as an :class:`int` or a :class:`float`,
or :obj:`None` if the glyph has no outlines.
.. note::
Subclasses may override this method.
"""
bounds = self.bounds
if bounds is None:
return None
xMin, yMin, xMax, yMax = bounds
return self.width - xMax
[docs]
def _set_rightMargin(self, value: IntFloatType) -> None:
"""Set the native glyph's right margin.
This is the environment implementation of
the :attr:`BaseGlyph.rightMargin` property setter.
:param value: The right glyph margin to set as an :class:`int` or
a :class:`float`.
.. note::
Subclasses may override this method.
"""
bounds = self.bounds
if bounds is None:
self.width = value
else:
xMin, yMin, xMax, yMax = bounds
self.width = xMax + value
# vertical
height: dynamicProperty = dynamicProperty(
"base_height",
"""Get or set the glyph's height.
The value must be :class:`int` or :class:`float`.
:return: The glyph height as an :class:`int` or a :class:`float`.
Example::
>>> glyph.height
500
>>> glyph.height = 200
""",
)
def _get_base_height(self) -> IntFloatType:
value = self._get_height()
value = normalizers.normalizeGlyphHeight(value)
return value
def _set_base_height(self, value: IntFloatType) -> None:
value = normalizers.normalizeGlyphHeight(value)
self._set_height(value)
[docs]
def _get_height(self) -> IntFloatType: # type: ignore[return]
"""Get the native glyph's height.
This is the environment implementation of
the :attr:`BaseGlyph.height` property getter.
:return: The glyph height as an :class:`int` or :class:`float`.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def _set_height(self, value: IntFloatType) -> None:
"""Set the native glyph's height.
This is the environment implementation of the :attr:`BaseGlyph.height`
property setter.
:param value: The glyph height as an :class:`int` or :class:`float`.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
bottomMargin: dynamicProperty = dynamicProperty(
"base_bottomMargin",
"""Get or set the glyph's bottom margin.
The value must be either an :class:`int` or a :class:`float`.
:return: The bottom glyph margin as an :class:`int` or a :class:`float`,
or :obj:`None` to indicate that the glyph has no outlines.
Example::
>>> glyph.bottomMargin
35
>>> glyph.bottomMargin = 45
""",
)
def _get_base_bottomMargin(self) -> IntFloatType | None:
value = self._get_bottomMargin()
value = normalizers.normalizeGlyphBottomMargin(value)
return value
def _set_base_bottomMargin(self, value: IntFloatType) -> None:
normalizedValue = normalizers.normalizeGlyphBottomMargin(value)
# Avoid mypy conflict with normalizeGlyphBottomMargin -> Optional[IntFloat]
if normalizedValue is None:
raise TypeError("The value for bottomMargin cannot be None.")
self._set_bottomMargin(normalizedValue)
[docs]
def _get_bottomMargin(self) -> IntFloatType | None:
"""Get the native glyph's bottom margin.
This is the environment implementation of
the :attr:`BaseGlyph.bottomMargin` property getter.
:return: The bottom glyph margin as an :class:`int` or a :class:`float`,
or :obj:`None` to indicate that the glyph has no outlines.
.. note::
Subclasses may override this method.
"""
bounds = self.bounds
if bounds is None:
return None
xMin, yMin, xMax, yMax = bounds
return yMin
[docs]
def _set_bottomMargin(self, value: IntFloatType) -> None:
"""Set the native glyph's bottom margin.
This is the environment implementation of
the :attr:`BaseGlyph.bottomMargin` property setter.
:param value: The bottom glyph margin to set as an :class:`int` or
a :class:`float`.
.. note::
Subclasses may override this method.
"""
diff = value - self.bottomMargin
self.moveBy((0, diff))
self.height += diff
topMargin: dynamicProperty = dynamicProperty(
"base_topMargin",
"""Get or set the glyph's top margin.
The value must be either an :class:`int` or a :class:`float`.
:return: The top glyph margin as an :class:`int` or a :class:`float`,
or :obj:`None` to indicate that the glyph has no outlines.
Example::
>>> glyph.topMargin
35
>>> glyph.topMargin = 45
""",
)
def _get_base_topMargin(self) -> IntFloatType | None:
value = self._get_topMargin()
value = normalizers.normalizeGlyphTopMargin(value)
return value
def _set_base_topMargin(self, value: IntFloatType) -> None:
normalizedValue = normalizers.normalizeGlyphTopMargin(value)
# Avoid mypy conflict with normalizeGlyphTopMargin -> Optional[IntFloat]
if normalizedValue is None:
raise TypeError("The value for topMargin cannot be None.")
self._set_topMargin(normalizedValue)
[docs]
def _get_topMargin(self) -> IntFloatType | None:
"""Get the native glyph's top margin.
This is the environment implementation of
the :attr:`BaseGlyph.topMargin` property getter.
:return: The top glyph margin as an :class:`int` or a :class:`float`,
or :obj:`None` to indicate that the glyph has no outlines.
.. note::
Subclasses may override this method.
"""
bounds = self.bounds
if bounds is None:
return None
xMin, yMin, xMax, yMax = bounds
return self.height - yMax
[docs]
def _set_topMargin(self, value: IntFloatType) -> None:
"""Set the native glyph's top margin.
This is the environment implementation of
the :attr:`BaseGlyph.topMargin` property setter.
:param value: The top glyph margin to set as an :class:`int` or
a :class:`float`.
.. note::
Subclasses may override this method.
"""
bounds = self.bounds
if bounds is None:
self.height = value
else:
xMin, yMin, xMax, yMax = bounds
self.height = yMax + value
# ----
# Pens
# ----
[docs]
def getPen(self) -> PenType:
"""Return a pen object for adding outline data to the glyph.
:return: An instance of an :class:`~fontTools.pens.basePen.AbstractPen`
subclass.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
Example::
>>> pen = glyph.getPen()
"""
self.raiseNotImplementedError()
[docs]
def getPointPen(self) -> PointPenType:
"""Return a point pen object for adding outline data to the glyph.
:return: An instance of
an :class:`~fontTools.pens.pointPen.AbstractPointPen` subclass.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
Example::
>>> pointPen = glyph.getPointPen()
"""
self.raiseNotImplementedError()
[docs]
def draw(
self, pen: PenType, contours: bool = True, components: bool = True
) -> None:
"""Draw the glyph's outline data to the given pen object.
:param pen: The :class:`~fontTools.pens.basePen.AbstractPen` subclass
instance to which the data should be drawn.
:param contours: Whether to draw data from the glyph's contours.
Defaults to :obj:`True`
:param components: Whether to draw data from the glyph's contours.
Defaults to :obj:`True`
Example::
>>> glyph.draw(pen)
>>> glyph.draw(pen, contours=False)
>>> glyph.draw(pen, components=False)
"""
if contours:
for contour in self:
contour.draw(pen)
if components:
for component in self.components:
component.draw(pen)
[docs]
def drawPoints(
self, pen: PointPenType, contours: bool = True, components: bool = True
) -> None:
"""Draw the glyph's outline data to the given point pen object.
:param pen: The :class:`~fontTools.pens.pointPen.AbstractPointPen`
subclass instance to which the data should be drawn.
:param contours: Whether to draw data from the glyph's contours.
Defaults to :obj:`True`
:param components: Whether to draw data from the glyph's contours.
Defaults to :obj:`True`.
Example::
>>> glyph.drawPoints(pointPen)
>>> glyph.drawPoints(pointPen, contours=False)
>>> glyph.drawPoints(pointPen, components=False)
"""
if contours:
for contour in self:
contour.drawPoints(pen)
if components:
for component in self.components:
component.drawPoints(pen)
# -----------------------------------------
# Contour, Component and Anchor Interaction
# -----------------------------------------
[docs]
def clear(
self,
contours: bool = True,
components: bool = True,
anchors: bool = True,
guidelines: bool = True,
image: bool = True,
) -> None:
"""Clear the glyph data.
This will clear:
- :attr:`contours`
- :attr:`components`
- :attr:`anchors`
- :attr:`guidelines`
- :attr:`image`
The clearing of portions of the glyph may be turned off with the listed
parameters.
:param contours: Whether to clear the glyph's contour data.
Defaults to :obj:`True`
:param components: Whether to clear the glyph's component data.
Defaults to :obj:`True`
:param anchors: Whether to clear the glyph's anchor data.
Defaults to :obj:`True`
:param guidelines: Whether to clear the glyph's guideline data.
Defaults to :obj:`True`
:param image: Whether to clear the glyph's image data.
Defaults to :obj:`True`
Example::
>>> glyph.clear()
>>> glyph.clear(guidelines=False)
"""
self._clear(
contours=contours,
components=components,
anchors=anchors,
guidelines=guidelines,
image=image,
)
[docs]
def _clear(
self,
contours: bool,
components: bool,
anchors: bool,
guidelines: bool,
image: bool,
) -> None:
"""Clear the native glyph data.
This is the environment implementation of :meth:`BaseGlyph.clear`.
:param contours: Whether to clear the glyph's contour data.
:param components: Whether to clear the glyph's component data.
:param anchors: Whether to clear the glyph's anchor data.
:param guidelines: Whether to clear the glyph's guideline data.
:param image: Whether to clear the glyph's image data.
.. note::
Subclasses may override this method.
"""
if contours:
self.clearContours()
if components:
self.clearComponents()
if anchors:
self.clearAnchors()
if guidelines:
self.clearGuidelines()
if image:
self.clearImage()
[docs]
def appendGlyph(
self, other: BaseGlyph, offset: CoordinateLike | None = None
) -> None:
"""Append data from `other` to new objects in the glyph.
This will append:
- :attr:`contours`
- :attr:`components`
- :attr:`anchors`
- :attr:`guidelines`
:param other: The :class:`BaseGlyph` instace containing the source
data to append.
:param offset: The x and y shift values to be applied to the
appended data as a :ref:`type-coordinate`, or :obj:`None`
representing an offset of ``(0, 0)``.
Example::
>>> glyph.appendGlyph(otherGlyph)
>>> glyph.appendGlyph(otherGlyph, (100, 0))
"""
if offset is None:
offset = (0, 0)
normalizedOffset = normalizers.normalizeTransformationOffset(offset)
self._appendGlyph(other, normalizedOffset)
[docs]
def _appendGlyph(self, other: BaseGlyph, offset: CoordinateLike) -> None:
"""Append data from `other` to new objects in the native glyph.
This is the environment implementation of :meth:`BaseGlyph.appendGlyph`.
:param other: The :class:`BaseGlyph` instace containing the source
data to append.
:param offset: The x and y shift values to be applied to the
appended data as a :ref:`type-coordinate`.
.. note::
Subclasses may override this method.
"""
other = other.copy()
if offset != (0, 0):
other.moveBy(offset)
for contour in other.contours:
self.appendContour(contour)
for component in other.components:
self.appendComponent(component=component)
for anchor in other.anchors:
self.appendAnchor(anchor=anchor)
for guideline in other.guidelines:
self.appendGuideline(guideline=guideline)
def _setGlyphInContour(self, contour: BaseContour) -> None:
if contour.glyph is None:
contour.glyph = self
contours: dynamicProperty = dynamicProperty(
"contours",
"""Get all contours in the glyph.
This property is read-only.
:return: A :class:`tuple` of :class:`BaseContour` objects.
Example::
>>> contours = glyph.contours
""",
)
[docs]
def _get_contours(self) -> tuple[BaseContour, ...]:
"""Get all contours in the native glyph.
This is the environment implementation of the :attr:`BaseGlyph.contours`
property getter.
:return: A :class:`tuple` of :class:`BaseContour` subclass instances.
.. note::
Subclasses may override this method.
"""
return tuple(self[i] for i in range(len(self)))
[docs]
def __len__(self) -> int:
"""Get the number of contours in the glyph.
:return: An :class:`int` representing the number of contours in the
glyph.
Example::
>>> len(glyph)
2
"""
return self._lenContours()
[docs]
def _lenContours(self, **kwargs: Any) -> int: # type: ignore[return]
r"""Get the number of contours in the native glyph.
This is the environment implementation of :meth:`BaseGlyph.__len__`.
:param \**kwargs: Additional keyword arguments.
:return: An :class:`int` representing the number of contours in the
glyph.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def __iter__(self) -> Iterator[BaseContour]:
"""Iterate through all contours in the glyph.
:return: An iterator of :class:`BaseContour` instances.
Example::
>>> for contour in glyph:
... contour.reverse()
"""
return self._iterContours()
[docs]
def _iterContours(self, **kwargs: Any) -> Iterator[BaseContour]:
r"""Iterate through all contours in the native glyph.
This is the environment implementation of :meth:`BaseGlyph.__iter__`.
:param \**kwargs: Additional keyword arguments.
:return: An iterator of :class:`BaseContour` subclass instances.
.. note::
Subclasses may override this method.
"""
count = len(self)
index = 0
while count:
yield self[index]
count -= 1
index += 1
[docs]
def __getitem__(self, index: int) -> BaseContour:
"""Get the contour located at the given index from the glyph.
:param index: The index of the glyph to return as an :class:`int`.
:return: An instance of the :class:`BaseContour` class.
Example::
>>> contour = glyph[0]
"""
normalizedIndex = normalizers.normalizeIndex(index)
if normalizedIndex is None or normalizedIndex >= len(self):
raise ValueError(f"No contour located at index {normalizedIndex}.")
contour = self._getContour(normalizedIndex)
self._setGlyphInContour(contour)
return contour
# type: ignore[return]
[docs]
def _getContour(self, index: int, **kwargs: Any) -> BaseContour:
r"""Get the contour located at the given index from the native glyph.
:param index: The index of the contour to return as an :class:`int`.
:param \**kwargs: Additional keyword arguments.
:return: An instance of a :class:`BaseContour` subclass.
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def _getContourIndex(self, contour: BaseContour) -> int:
for i, other in enumerate(self.contours):
if contour == other:
return i
raise FontPartsError("The contour could not be found.")
[docs]
def appendContour(
self, contour: BaseContour, offset: CoordinateLike | None = None
) -> BaseContour:
"""Append the given contour's data to the glyph.
:param contour: The :class:`BaseContour` instace containing the source
data to append.
:param offset: The x and y shift values to be applied to the
appended data as a :ref:`type-coordinate`,
or :obj:`None` representing an offset of ``(0, 0)``.
:return: A :class:`BaseContour` instance containing the appended data.
Example::
>>> contour = glyph.appendContour(contour)
>>> contour = glyph.appendContour(contour, (100, 0))
"""
normalizedContour = normalizers.normalizeContour(contour)
if offset is None:
offset = (0, 0)
normalizedOffset = normalizers.normalizeTransformationOffset(offset)
return self._appendContour(normalizedContour, normalizedOffset)
[docs]
def _appendContour(
self, contour: BaseContour, offset: CoordinateLike, **kwargs: Any
) -> BaseContour:
r"""Append the given contour's data to the native glyph.
This is the environment implementation of :meth:`BaseGlyph.appendContour`.
:param contour: The :class:`BaseContour` instace containing the source
data to append.
:param offset: The x and y shift values to be applied to the appended
data as a :ref:`type-coordinate`.
:param \**kwargs: Additional keyword arguments.
:return: A :class:`BaseContour` instance containing the appended data.
.. note::
Subclasses may override this method.
"""
pointPen = self.getPointPen()
if offset != (0, 0):
copy = contour.copy()
copy.moveBy(offset)
copy.drawPoints(pointPen)
else:
contour.drawPoints(pointPen)
return self[-1]
[docs]
def removeContour(self, contour: BaseContour | int) -> None:
"""Remove the given contour from the glyph.
:param contour: The contour to remove as a :class:`BaseContour`
instance or an :class:`int` representing a contour index.
:raises ValueError: If no contour can be found at the given `index`.
Example::
>>> glyph.removeContour(contour)
"""
if isinstance(contour, int):
index = contour
else:
index = self._getContourIndex(contour)
normalizedIndex = normalizers.normalizeIndex(index)
# Avoid mypy conflict with normalizeIndex -> Optional[int]
if normalizedIndex is None: # pragma: no cover
return
if normalizedIndex >= len(self):
raise ValueError(f"No contour located at index {normalizedIndex}.")
self._removeContour(normalizedIndex)
[docs]
def _removeContour(self, index: int, **kwargs: Any) -> None:
r"""Remove the given contour from the native glyph.
This is the environment implementation
of :meth:`BaseGlyph.removeContour`.
:param contour: The contour to remove as a :class:`BaseContour`
instance or an :class:`int` representing a contour index.
: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()
[docs]
def clearContours(self) -> None:
"""Clear all contours in the glyph.
Example::
>>> glyph.clearContours()
"""
self._clearContours()
[docs]
def _clearContours(self) -> None:
"""Clear all contours in the native glyph.
This is the environment implementation
of :meth:`BaseGlyph.clearContours`.
.. note::
Subclasses may override this method.
"""
for _ in range(len(self)):
self.removeContour(-1)
[docs]
def removeOverlap(self) -> None:
"""Perform a remove overlap operation on the glyph's contours.
The behavior of this may vary across environments.
Example::
>>> glyph.removeOverlap()
"""
self._removeOverlap()
[docs]
def _removeOverlap(self) -> None:
"""Perform a remove overlap operation on the native glyph's contours.
This is the environment implementation
of :meth:`BaseGlyph.removeOverlap`.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must implement this method.
"""
self.raiseNotImplementedError()
# Components
def _setGlyphInComponent(self, component: BaseComponent) -> None:
if component.glyph is None:
component.glyph = self
components: dynamicProperty = dynamicProperty(
"components",
"""Get all components in the glyph.
This property is read-only.
:return: A :class:`tuple` of :class:`BaseComponent` instances.
Example::
>>> components = glyph.components
""",
)
[docs]
def _get_components(self) -> tuple[BaseComponent, ...]:
"""Get all components in the native glyph.
This is the environment implementation of
the :attr:`BaseGlyph.components` property getter.
:return: A :class:`tuple` of :class:`BaseComponent` subclass instances.
.. note::
Subclasses may override this method.
"""
return tuple(
self._getitem__components(i) for i in range(self._len__components())
)
def _len__components(self) -> int:
return self._lenComponents()
[docs]
def _lenComponents(self, **kwargs: Any) -> int: # type: ignore[return]
r"""Get the number of components in the glyph.
:param \**kwargs: Additional keyword arguments.
:return: An :class:`int` indicating the number of components in the
glyph.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def _getitem__components(self, index: int) -> BaseComponent:
normalizedIndex = normalizers.normalizeIndex(index)
if normalizedIndex is None or normalizedIndex >= self._len__components():
raise ValueError(f"No component located at index {normalizedIndex}.")
component = self._getComponent(normalizedIndex)
self._setGlyphInComponent(component)
return component
# type: ignore[return]
[docs]
def _getComponent(self, index: int, **kwargs: Any) -> BaseComponent:
r"""Get the component at the given index from the native glyph.
:param index: The index of the component to return as an :class:`int`.
:param \**kwargs: Additional keyword arguments.
:return: An instance of a :class:`BaseComponent` subclass.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def _getComponentIndex(self, component: BaseComponent) -> int:
for i, other in enumerate(self.components):
if component == other:
return i
raise FontPartsError("The component could not be found.")
[docs]
def appendComponent(
self,
baseGlyph: str | None = None,
offset: CoordinateLike | None = None,
scale: ScaleFactorLike | None = None,
component: BaseComponent | None = None,
) -> BaseComponent:
"""Append a component to the glyph.
If `baseGlyph`, `offset` or `scale` is specified, those values will be
used instead of the values in the given `component`.
:param baseGlyph: An optional glyph name to append as a component.
Defaults to :obj:`None`.
:param offset: The x and y shift values to be applied to the
appended data as a :ref:`type-coordinate`,
or :obj:`None` representing an offset of ``(0, 0)``.
Defaults to :obj:`None`
:param scale: The x and y scale values that should be applied to
the appended component as a :class:`tuple` of :class:`int`
or :class:`float` values, or :obj:`None` representing a scale of
``(1.0, 1.0)``. Defaults to :obj:`None`
:param component: An optional :class:`BaseComponent` instance from which
to copy attribute values. Defaults to :obj:`None`.
:return: The newly appended :class:`BaseComponent` instance.
:raises FontPartsError: If the `baseGlyph` refers to the current glyph
instance, which would result in a component referencing itself. This
is not permitted.
Example::
>>> component = glyph.appendComponent("A")
>>> component = glyph.appendComponent("A", offset=(10, 20))
>>> component = glyph.appendComponent("A", scale=(1.0, 2.0))
"""
identifier = None
sxy = 0
syx = 0
if component is not None:
normalizedComponent = normalizers.normalizeComponent(component)
if baseGlyph is None:
baseGlyph = normalizedComponent.baseGlyph
sx, sxy, syx, sy, ox, oy = normalizedComponent.transformation
if offset is None:
offset = (ox, oy)
if scale is None:
scale = (sx, sy)
if baseGlyph is None:
baseGlyph = normalizedComponent.baseGlyph
if normalizedComponent.identifier is not None:
existing = {
c.identifier for c in self.components if c.identifier is not None
}
if normalizedComponent.identifier not in existing:
identifier = normalizedComponent.identifier
if baseGlyph is not None:
normalizedBaseGlyph = normalizers.normalizeGlyphName(baseGlyph)
if self.name == normalizedBaseGlyph:
raise FontPartsError(
"A glyph cannot contain a component referencing itself."
)
if offset is None:
offset = (0, 0)
if scale is None:
scale = (1, 1)
normalizedOffset = normalizers.normalizeTransformationOffset(offset)
normalizedScale = normalizers.normalizeTransformationScale(scale)
ox, oy = normalizedOffset
sx, sy = normalizedScale
transformation = (sx, sxy, syx, sy, ox, oy)
normalizedIdentifier = normalizers.normalizeIdentifier(identifier)
return self._appendComponent(
normalizedBaseGlyph,
transformation=transformation,
identifier=normalizedIdentifier,
)
[docs]
def _appendComponent(
self,
baseGlyph: str,
transformation: AffineTransformationLike | None,
identifier: str | None,
**kwargs: Any,
) -> BaseComponent:
r"""Append a component to the native glyph.
This is the environment implementation of :meth:`BaseGlyph.appendComponent`.
:param baseGlyph: The glyph name to append as a component.
:param transformation: The :ref:`type-transformation` values to be applied
to the appended data or :obj:`None`.
:param identifier: A valid, nonconflicting :ref:`type-identifier` as
a :clss:`str` or :obj:`None`.
:param \**kwargs: Additional keyword arguments.
:return: The newly appended :class:`BaseComponent` subclass instance.
.. note::
Subclasses may override this method.
"""
pointPen = self.getPointPen()
pointPen.addComponent(
baseGlyph, transformation=transformation, identifier=identifier
)
return self.components[-1]
[docs]
def removeComponent(self, component: BaseComponent | int) -> None:
"""Remove the specified component from the glyph.
:param component: The component to remove as a :class:`BaseComponent`
instance or an :class:`int` representing the component's index.
:raises ValueError: If no component can be found at the given `index`.
Example::
>>> glyph.removeComponent(component)
"""
if isinstance(component, int):
index = component
else:
index = self._getComponentIndex(component)
normalizedIndex = normalizers.normalizeIndex(index)
# Avoid mypy conflict with normalizeIndex -> Optional[int]
if normalizedIndex is None: # pragma: no cover
return
if normalizedIndex >= self._len__components():
raise ValueError(f"No component located at index {normalizedIndex}.")
self._removeComponent(normalizedIndex)
[docs]
def _removeComponent(self, index: int, **kwargs: Any) -> None:
r"""Remove the specified component from the native glyph.
This is the environment implementation of :meth:`BaseGlyph.removeComponent`.
:param index: The index of the component to remove as an :class:`int`.
: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()
[docs]
def clearComponents(self) -> None:
"""Clear all components in the glyph.
Example::
>>> glyph.clearComponents()
"""
self._clearComponents()
[docs]
def _clearComponents(self) -> None:
"""Clear all components in the native glyph.
This is the environment implementation
of :meth:`BaseGlyph.clearComponents`.
.. note::
Subclasses may override this method.
"""
for _ in range(self._len__components()):
self.removeComponent(-1)
[docs]
def decompose(self) -> None:
"""Decompose all components in the glyph to contours.
Example::
>>> glyph.decompose()
"""
self._decompose()
[docs]
def _decompose(self) -> None:
"""Decompose all components in the native glyph to contours.
.. note::
Subclasses may override this method.
"""
for component in self.components:
component.decompose()
# Anchors
def _setGlyphInAnchor(self, anchor: BaseAnchor) -> None:
if anchor.glyph is None:
anchor.glyph = self
anchors: dynamicProperty = dynamicProperty(
"anchors",
"""Get all anchors in the glyph.
This property is read-only.
:return: A :class:`tuple` of :class:`BaseAnchor` instances.
Example::
>>> anchors = glyph.anchors
""",
)
[docs]
def _get_anchors(self) -> tuple[BaseAnchor, ...]:
"""Get all anchors in the native glyph.
:return: A :class:`tuple` of :class:`BaseAnchor` subclass instances.
.. note::
Subclasses may override this method.
"""
return tuple(self._getitem__anchors(i) for i in range(self._len__anchors()))
def _len__anchors(self) -> int:
return self._lenAnchors()
[docs]
def _lenAnchors(self, **kwargs: Any) -> int: # type: ignore[return]
r"""Get the number of anchors in the ntive glyph.
:param \**kwargs: Additional keyword arguments.
:return: An :class:`int` indicating the number of anchors in the glyph.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def _getitem__anchors(self, index: int) -> BaseAnchor:
normalizedIndex = normalizers.normalizeIndex(index)
if normalizedIndex is None or normalizedIndex >= self._len__anchors():
raise ValueError(f"No anchor located at index {normalizedIndex}.")
anchor = self._getAnchor(normalizedIndex)
self._setGlyphInAnchor(anchor)
return anchor
# type: ignore[return]
[docs]
def _getAnchor(self, index: int, **kwargs: Any) -> BaseAnchor:
r"""Get the anchor at the given index from the native glyph.
:param index: The index of the anchor to get as an :class:`int`.
:param \**kwargs: Additional keyword arguments.
:return: An instance of a :class:`BaseAnchor` subclass.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def _getAnchorIndex(self, anchor: BaseAnchor) -> int:
for i, other in enumerate(self.anchors):
if anchor == other:
return i
raise FontPartsError("The anchor could not be found.")
[docs]
def appendAnchor(
self,
name: str | None = None,
position: CoordinateLike | None = None,
color: RGBALike | None = None,
anchor: BaseAnchor | None = None,
) -> BaseAnchor:
"""Append an anchor to the glyph.
If `name`, `position` or `color` are specified, those values will be
used instead of the values in the given anchor object.
:param name: An optional name to be assigned to the anchor as
a :class:`str`. Defaults to :obj:`None`.
:param position: The optional x and y location to be applied to the
anchor as a :ref:`type-coordinate`. Defaults to :obj:`None`.
:param color: The optional color to be applied to the anchor as
a :ref:`type-color`. Defaults to :obj:`None`.
:param anchor: An optional :class:`BaseAnchor` instance from which
attribute values will be copied. Defualts to :obj:`None`.
:return: The newly appended :class:`BaseAnchor` instance.
Example::
>>> anchor = glyph.appendAnchor("top", (10, 20))
>>> anchor = glyph.appendAnchor("top", (10, 20), color=(1, 0, 0, 1))
"""
identifier = None
if anchor is not None:
anchor = normalizers.normalizeAnchor(anchor)
if name is None:
name = anchor.name
if position is None:
position = anchor.position
if color is None:
color = anchor.color
if anchor.identifier is not None:
existing = {
a.identifier for a in self.anchors if a.identifier is not None
}
if anchor.identifier not in existing:
identifier = anchor.identifier
if name is not None:
name = normalizers.normalizeAnchorName(name)
else:
raise ValueError("Name can not be None.")
if position is not None:
position = normalizers.normalizeCoordinateTuple(position)
if color is not None:
color = normalizers.normalizeColor(color)
identifier = normalizers.normalizeIdentifier(identifier)
return self._appendAnchor(
name, position=position, color=color, identifier=identifier
)
[docs]
def _appendAnchor(
self, # type: ignore[return]
name: str,
position: CoordinateLike | None,
color: RGBALike | None,
identifier: str | None,
**kwargs: Any,
) -> BaseAnchor:
r"""Append an anchor to the native glyph.
This is the environment implementation of :meth:`BaseGlyph.appendAnchor`.
:param name: The name to be assigned to the anchor as a :class:`str`
or :obj:`None`.
:param position: The x and y location to be applied to the anchor as
a :ref:`type-coordinate` or :obj:`None`.
:param color: The color to be applied to the anchor as
a :ref:`type-color` or :obj:`None`.
:param identifier: A valid, nonconflicting :ref:`type-identifier` as
a :clss:`str` or :obj:`None`.
:param \**kwargs: Additional keyword arguments.
:return: The newly appended :class:`BaseAnchor` subclass instance.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
.. note::
Subclasses may override this method.
"""
self.raiseNotImplementedError()
[docs]
def removeAnchor(self, anchor: BaseAnchor | int) -> None:
"""Remove the given anchor from the glyph.
:param anchor: The anchor to remove as a :class:`BaseAnchor` intance,
or an :class:`int` representing the anchor's index.
:raises ValueError: If no anchor can be found at the given `index`.
Example::
>>> glyph.removeAnchor(anchor)
"""
if isinstance(anchor, int):
index = anchor
else:
index = self._getAnchorIndex(anchor)
normalizedIndex = normalizers.normalizeIndex(index)
# Avoid mypy conflict with normalizeIndex -> Optional[int]
if normalizedIndex is None: # pragma: no cover
return
if normalizedIndex >= self._len__anchors():
raise ValueError(f"No anchor located at index {normalizedIndex}.")
self._removeAnchor(normalizedIndex)
[docs]
def _removeAnchor(self, index: int, **kwargs: Any) -> None:
r"""Remove the given anchor from the glyph.
This is the environment implementation of :meth:`BaseGlyph.removeAnchor`.
:param index: The index of the anchor to remove as an :class:`int`.
: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()
[docs]
def clearAnchors(self) -> None:
"""Clear all anchors in the glyph.
Example::
>>> glyph.clearAnchors()
"""
self._clearAnchors()
[docs]
def _clearAnchors(self) -> None:
"""Clear all anchors in the native glyph.
This is the environment implementation of :meth:`BaseGlyph.clearAnchors`.
.. note::
Subclasses may override this method.
"""
for _ in range(self._len__anchors()):
self.removeAnchor(-1)
# ----------
# Guidelines
# ----------
def _setGlyphInGuideline(self, guideline: BaseGuideline) -> None:
if guideline.glyph is None:
guideline.glyph = self
guidelines: dynamicProperty = dynamicProperty(
"guidelines",
"""Get all guidelines in the glyph.
This property is read-only.
:return: A :class:`tuple` of :class:`BaseGuideline` instances.
Example::
>>> guidelines = glyph.guidelines
""",
)
[docs]
def _get_guidelines(self) -> tuple[BaseGuideline, ...]:
"""Get all guidelines in the glyph.
This is the environment implementation of
the :attr:`BaseGlyph.guidelines` property getter.
:return: A :class:`tuple` of :class:`BaseGuideline` subclass instances.
.. note::
Subclasses may override this method.
"""
return tuple(
self._getitem__guidelines(i) for i in range(self._len__guidelines())
)
def _len__guidelines(self) -> int:
return self._lenGuidelines()
[docs]
def _lenGuidelines(self, **kwargs: Any) -> int: # type: ignore[return]
r"""Get the number of guidelines in the ntive glyph.
:param \**kwargs: Additional keyword arguments.
:return: An :class:`int` indicating the number of guidelines in the
glyph.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def _getitem__guidelines(self, index: int) -> BaseGuideline:
normalizedIndex = normalizers.normalizeIndex(index)
if normalizedIndex is None or normalizedIndex >= self._len__guidelines():
raise ValueError(f"No guideline located at index {normalizedIndex}.")
guideline = self._getGuideline(normalizedIndex)
self._setGlyphInGuideline(guideline)
return guideline
# type: ignore[return]
[docs]
def _getGuideline(self, index: int, **kwargs: Any) -> BaseGuideline:
r"""Get the guideline at the given index from the native glyph.
:param index: The index of the guideline to get as an :class:`int`.
:param \**kwargs: Additional keyword arguments.
:return: An instance of a :class:`BaseGuideline` subclass.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def _getGuidelineIndex(self, guideline: BaseGuideline) -> int:
for i, other in enumerate(self.guidelines):
if guideline == other:
return i
raise FontPartsError("The guideline could not be found.")
[docs]
def appendGuideline(
self,
position: CoordinateLike | None = None,
angle: IntFloatType | None = None,
name: str | None = None,
color: RGBALike | None = None,
guideline: BaseGuideline | None = None,
) -> BaseGuideline:
"""Append a guideline to the glyph.
If `name`, `position` or `color` are specified, those values will be
used instead of the values in the given guideline object.
:param position: The optional x and y location to be applied to the
guideline as a :ref:`type-coordinate`. Defaults to :obj:`None`.
:param angle: The optional angle to be applied to the guideline
as :class:`int` or :class:`float`. Defaults to :obj:`None`.
:param name: An optional name to be assigned to the guideline as
a :class:`str`. Defaults to :obj:`None`.
:param color: The optional color to be applied to the guideline as
a :ref:`type-color`. Defaults to :obj:`None`.
:param guideline: An optional :class:`BaseGuideline` instance from which
attribute values will be copied. Defualts to :obj:`None`.
:return: The newly appended :class:`BaseGuideline` instance.
Example::
>>> anchor = glyph.appendGuideline("top", (10, 20))
>>> anchor = glyph.appendGuideline("top", (10, 20), color=(1, 0, 0, 1))
"""
identifier: str | None = None
if guideline is not None:
guideline = normalizers.normalizeGuideline(guideline)
if position is None:
position = guideline.position
if angle is None:
angle = guideline.angle
if name is None:
name = guideline.name
if color is None:
color = guideline.color
if guideline.identifier is not None:
existing = {
g.identifier for g in self.guidelines if g.identifier is not None
}
if guideline.identifier not in existing:
identifier = guideline.identifier
if position is not None:
position = normalizers.normalizeCoordinateTuple(position)
else:
raise ValueError("Position can not be None.")
if angle is not None:
angle = normalizers.normalizeRotationAngle(angle)
else:
raise ValueError("Angle can not be None.")
if name is not None:
name = normalizers.normalizeGuidelineName(name)
else:
name = None
if color is not None:
color = normalizers.normalizeColor(color)
else:
color = None
identifier = normalizers.normalizeIdentifier(identifier)
newGuideline = self._appendGuideline(
position, angle, name=name, color=color, identifier=identifier
)
newGuideline.glyph = self
return newGuideline
[docs]
def _appendGuideline(
self, # type: ignore[return]
position: CoordinateLike,
angle: IntFloatType,
name: str | None,
color: RGBALike | None,
identifier: str | None,
**kwargs: Any,
) -> BaseGuideline:
r"""Append a guideline to the native glyph.
This is the environment implementation of :meth:`BaseGlyph.appendGuideline`.
:param position: The x and y location to be applied to the
guideline as a :ref:`type-coordinate`.
:param angle: The angle to be applied to the guideline
as :class:`int` or :class:`float`.
:param name: The name to be assigned to the guideline as a :class:`str`
or :obj:`None`.
:param color: The color to be applied to the guideline as
a :ref:`type-color` or :obj:`None`.
:param identifier: An optioanal valid, nonconflicting :ref:`type-identifier`
as a :clss:`str` or :obj:`None`.
:param \**kwargs: Additional keyword arguments.
:return: The newly appended :class:`BaseGuideline` subclass instance.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
.. note::
Subclasses may override this method.
"""
self.raiseNotImplementedError()
[docs]
def removeGuideline(self, guideline: BaseGuideline | int) -> None:
"""Remove the given guideline from the glyph.
:param guideline: The guideline to remove as a :class:`BaseGuideline`
intance, or an :class:`int` representing the guideline's index.
:raises ValueError: If no guideline can be found at the given `index`.
Example::
>>> glyph.removeGuideline(guideline)
"""
if isinstance(guideline, int):
index = guideline
else:
index = self._getGuidelineIndex(guideline)
normalizedIndex = normalizers.normalizeIndex(index)
if normalizedIndex is None: # pragma: no cover
return
if normalizedIndex >= self._len__guidelines():
raise ValueError(f"No guideline located at index {normalizedIndex}.")
self._removeGuideline(normalizedIndex)
[docs]
def _removeGuideline(self, index: int, **kwargs: Any) -> None:
r"""Remove the given guideline from the glyph.
This is the environment implementation of :meth:`BaseGlyph.removeGuideline`.
:param index: The index of the guideline to remove as an :class:`int`.
: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()
[docs]
def clearGuidelines(self) -> None:
"""Clear all guidelines in the glyph.
Example::
>>> glyph.clearGuidelines()
"""
self._clearGuidelines()
[docs]
def _clearGuidelines(self) -> None:
"""Clear all guidelines in the native glyph.
This is the environment implementation of :meth:`BaseGlyph.clearGuidelines`.
.. note::
Subclasses may override this method.
"""
for _ in range(self._len__guidelines()):
self.removeGuideline(-1)
# ------------------
# Data Normalization
# ------------------
[docs]
def round(self) -> None:
"""Round coordinates in the glyph to the nearest integer.
This applies to:
- :attr:`width`
- :attr:`height`
- :attr:`contours`
- :attr:`components`
- :attr:`anchors`
- :attr:`guidelines`
- :attr:`image`
Example::
>>> glyph.round()
"""
self._round()
[docs]
def _round(self) -> None:
"""Round coordinates in the native glyph to the nearest integer.
This is the environment implementation of :meth:`BaseGlyph.round`.
.. note::
Subclasses may override this method.
"""
for contour in self.contours:
contour.round()
for component in self.components:
component.round()
for anchor in self.anchors:
anchor.round()
for guideline in self.guidelines:
guideline.round()
if self.image.data is not None:
self.image.round()
self.width = normalizers.normalizeVisualRounding(self.width)
self.height = normalizers.normalizeVisualRounding(self.height)
def correctDirection(self, trueType: bool = False) -> None:
"""Correct the winding direction of the glyph's contours.
By default this method follows the PostScript winding
recommendations.
:param trueType: Whether to follow TrueType rather than PostScript
winding recommendations. Defaults to :obj:`False`.
Example::
>>> glyph.correctDirection()
"""
self._correctDirection(trueType=trueType)
def _correctDirection(self, trueType: bool, **kwargs: Any) -> None:
r"""Correct the winding direction of the native glyph's contours.
This is the environment implementation
of :meth:`BaseGlyph.correctDirection`.
:param trueType: Whether to follow TrueType rather than PostScript
winding recommendations.
:param \**kwargs: Additional keyword arguments.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
.. note::
Subclasses may override this method.
"""
self.raiseNotImplementedError()
def autoContourOrder(self) -> None:
"""Automatically order the glyph's contours based on heuristics.
The results of this may vary across environments.
Example::
>>> glyph.autoContourOrder()
"""
self._autoContourOrder()
def _autoContourOrder(self, **kwargs: Any) -> None:
r"""Automatically order the native glyph's contours based on heuristics.
This is the environment implementation of :meth:`BaseGlyph.autoContourOrder`.
Sorting is based on (in this order):
- the (negative) point count
- the (negative) segment count
- x value of the center of the contour rounded to a threshold
- y value of the center of the contour rounded to a threshold
(such threshold is calculated as the smallest contour width
or height in the glyph divided by two)
- the (negative) surface of the bounding box of the contour: ``width * height``
The latter is a safety net for instances like a very thin 'O' where the
x centers could be close enough to rely on the y for the sort, which could
very well be the same for both contours. We use the *negative* of the surface
to ensure that larger contours appear first, which seems more natural.
:param \**kwargs: Additional keyword arguments.
"""
tempContourList: TempContourListType = []
contourList: ContourListType = []
xThreshold: IntFloatType | None = None
yThreshold: IntFloatType | None = None
for contour in self:
bounds = contour.bounds
if bounds is None:
continue
xMin, yMin, xMax, yMax = bounds
width = xMax - xMin
height = yMax - yMin
xC = 0.5 * (xMin + xMax)
yC = 0.5 * (yMin + yMax)
xTh = abs(width * 0.5)
yTh = abs(height * 0.5)
if xThreshold is None or xThreshold > xTh:
xThreshold = xTh
if yThreshold is None or yThreshold > yTh:
yThreshold = yTh
tempContourList.append(
(
-len(contour.points),
-len(contour.segments),
xC,
yC,
-(width * height),
contour,
)
)
xThreshold = xThreshold or 0.0
yThreshold = yThreshold or 0.0
for points, segments, x, y, surface, contour in tempContourList:
contourList.append(
(
points,
segments,
FuzzyNumber(x, xThreshold),
FuzzyNumber(y, yThreshold),
surface,
contour,
)
)
contourList.sort()
self.clearContours()
for points, segments, xO, yO, surface, contour in contourList:
self.appendContour(contour)
# --------------
# Transformation
# --------------
[docs]
def scaleBy(
self,
value: ScaleFactorLike,
origin: CoordinateLike | None = None,
width: bool = False,
height: bool = False,
) -> None:
"""Scale the glyph according to the given values.
:param value: The x and y values to scale the glyph by as
a :class:`tuple` of two :class:`int` or :class:`float` values.
:param origin: The optional point at which the scale should originate as
a :ref:`type-coordinate`. This must not be set when scaling the width
or height. Defaults to :obj:`None`, representing an origin of ``(0, 0)``.
:param width: Whether the glyph's width should be scaled.
Defaults to :obj:`False`.
:param height: Whether the glyph's height should be scaled.
Defaults to :obj:`False`.
:raises FontPartsError: If the `origin` is specified while `width` or
`height` are set to :obj:`True`.
Example::
>>> glyph.scaleBy(2.0)
>>> glyph.scaleBy((0.5, 2.0), origin=(500, 500))
"""
normalizedValue = normalizers.normalizeTransformationScale(value)
if origin is None:
origin = (0, 0)
normalizedOrigin = normalizers.normalizeCoordinateTuple(origin)
if normalizedOrigin != (0, 0) and (width or height):
raise FontPartsError(
"The origin must not be set when scaling the width or height."
)
super().scaleBy(normalizedValue, origin=normalizedOrigin)
sX, sY = normalizedValue
if width:
self._scaleWidthBy(sX)
if height:
self._scaleHeightBy(sY)
def _scaleWidthBy(self, value: IntFloatType) -> None:
"""Scale the glyph's width according to the given value.
:param value: The value to scale the glyph width by as an :class:`int`
or :class:`float`.
.. note::
Subclasses may override this method.
"""
self.width *= value
def _scaleHeightBy(self, value: IntFloatType) -> None:
"""Scale the glyph's height according to the given value.
:param value: The value to scale the glyph height by as an :class:`int`
or :class:`float`.
.. note::
Subclasses may override this method.
"""
self.height *= value
# --------------------
# Interpolation & Math
# --------------------
def toMathGlyph(
self, scaleComponentTransform: bool = True, strict: bool = False
) -> MathGlyph:
"""Return the glyph as a `fontMath <https://github.com/typesupply/fontMath>`_ :class:`MathGlyph` object.
This method returns the glyph as an object following the `MathGlyph
protocol <https://github.com/typesupply/fontMath>`_.
:param scaleComponentTransform: Whether to enable
the :attr:`fontMath.MathGlyph.scaleComponentTransform` option.
:param strict: Whether to enable the :attr:`fontMath.MathGlyph.strict`
option.
:return: A :class:`fontMath.MathGlyph` object representing the current
glyph.
Example::
>>> mg = glyph.toMathGlyph()
"""
return self._toMathGlyph(
scaleComponentTransform=scaleComponentTransform, strict=strict
)
def _toMathGlyph(self, scaleComponentTransform: bool, strict: bool) -> MathGlyph:
"""Return the native glyph as a MathGlyph object.
This is the environment implementation of :meth:`BaseGlyph.toMathGlyph`.
:param scaleComponentTransform: Whether to enable
the :attr:`fontMath.MathGlyph.scaleComponentTransform` option.
:param strict: Whether to enable the :attr:`fontMath.MathGlyph.strict`
option.
:return: A :class:`fontMath.MathGlyph` object representing the current
native glyph.
.. note::
Subclasses may override this method.
"""
mathGlyph = MathGlyph(
None, scaleComponentTransform=scaleComponentTransform, strict=strict
)
pen = mathGlyph.getPointPen()
self.drawPoints(pen)
for anchor in self.anchors:
d = dict(
x=anchor.x,
y=anchor.y,
name=anchor.name,
identifier=anchor.identifier,
color=anchor.color,
)
mathGlyph.anchors.append(d)
for guideline in self.guidelines:
d = dict(
x=guideline.x,
y=guideline.y,
angle=guideline.angle,
name=guideline.name,
identifier=guideline.identifier,
color=guideline.color,
)
mathGlyph.guidelines.append(d)
mathGlyph.lib = deepcopy(self.lib)
mathGlyph.name = self.name
mathGlyph.unicodes = self.unicodes
mathGlyph.width = self.width
mathGlyph.height = self.height
mathGlyph.note = self.note
return mathGlyph
def fromMathGlyph(
self, mathGlyph: MathGlyph, filterRedundantPoints: bool = True
) -> BaseGlyph:
"""Replace the glyph's data with the specified mathGlyph.
This method returns the glyph as an object following the `MathGlyph
protocol <https://github.com/typesupply/fontMath>`_.
:param mathGlyph: The :class:`fontMath.MathGlyph` object containing the
replacement data.
:param filterRedundantPoints: Whether to enable the
`filterRedundantPoints` option of the
:meth:`fontMath.MathGlyph.drawPoints` method. Defaults
to :obj:`True`.
:return: The newly updated :class:`BaseGlyph` instance.
Example::
>>> glyph.fromMathGlyph(mg)
"""
return self._fromMathGlyph(
mathGlyph, toThisGlyph=True, filterRedundantPoints=filterRedundantPoints
)
def _fromMathGlyph(
self, mathGlyph: MathGlyph, toThisGlyph: bool, filterRedundantPoints: bool
) -> BaseGlyph:
"""Replace native glyph data with the specified mathGlyph's data.
This is the environment implementation of :meth:`BaseGlyph.fromMathGlyph`.
:param mathGlyph: The object containing the replacement data. This must
be an object following the `MathGlyph protocol
<https://github.com/typesupply/fontMath>`_.
:param toThisGlyph: Whether to apply `mathGlyph` to the current glyph
instance or to a new glyph copy.
:param filterRedundantPoints: Whether to enable the
`filterRedundantPoints` option of the specified `mathGlyph`
object's :meth:`dreawPoints` method.
:return: The newly updated :class:`BaseGlyph` instance.
"""
# make the destination
if toThisGlyph:
copied = self
copied.clear()
else:
copyClass = self.copyClass
if copyClass is None:
copyClass = self.__class__
copied = copyClass()
# populate
pen = copied.getPointPen()
mathGlyph.drawPoints(pen, filterRedundantPoints=filterRedundantPoints)
for anchor in mathGlyph.anchors:
a = copied.appendAnchor(
name=anchor.get("name"),
position=(anchor["x"], anchor["y"]),
color=anchor.get("color"),
)
identifier = anchor.get("identifier")
if identifier is not None:
a._setIdentifier(identifier)
for guideline in mathGlyph.guidelines:
guideColor = guideline.get("color")
if guideColor is None:
colorData = None
else:
r, g, b, alpha = guideColor
colorData = (r, g, b, alpha)
g = copied.appendGuideline(
position=(guideline["x"], guideline["y"]),
angle=guideline["angle"],
name=guideline.get("name"),
color=colorData,
)
identifier = guideline.get("identifier")
if identifier is not None:
g._setIdentifier(identifier)
copied.lib.update(mathGlyph.lib)
if not toThisGlyph:
copied.name = mathGlyph.name
copied.unicodes = mathGlyph.unicodes
copied.width = mathGlyph.width
copied.height = mathGlyph.height
copied.note = mathGlyph.note
return copied
[docs]
def __mul__(self, factor: InterpolationFactorLike) -> BaseGlyph:
"""Multiply the current glyph by a given factor.
:param factor: The factor by which to multiply the glyph 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)``.
:return: The newly multiplied :class:`BaseGlyph` instance.
.. note::
Subclasses may override this method.
"""
mathGlyph = self._toMathGlyph(scaleComponentTransform=True, strict=False)
result = mathGlyph * factor
copied = self._fromMathGlyph(
result, toThisGlyph=False, filterRedundantPoints=True
)
return copied
__rmul__ = __mul__
def __truediv__(self, factor: InterpolationFactorLike) -> BaseGlyph:
"""Divide the current glyph by a given factor.
:param factor: The factor by which to divide the glyph 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)``.
:return: The newly divided :class:`BaseGlyph` instance.
.. note::
Subclasses may override this method.
"""
mathGlyph = self._toMathGlyph(scaleComponentTransform=True, strict=False)
result = mathGlyph / factor
copied = self._fromMathGlyph(
result, toThisGlyph=False, filterRedundantPoints=True
)
return copied
# py2 support
__div__ = __truediv__
[docs]
def __add__(self, other: BaseGlyph) -> BaseGlyph:
"""Add another glyph to the current glyph.
:param other: The :class:`BaseGlyph` instance to add to the current glyph.
:return: A new :class:`BaseGlyph` instance representing the added results.
.. note::
Subclasses may override this method.
"""
selfMathGlyph = self._toMathGlyph(scaleComponentTransform=True, strict=False)
otherMathGlyph = other._toMathGlyph(scaleComponentTransform=True, strict=False)
result = selfMathGlyph + otherMathGlyph
copied = self._fromMathGlyph(
result, toThisGlyph=False, filterRedundantPoints=True
)
return copied
[docs]
def __sub__(self, other: BaseGlyph) -> BaseGlyph:
"""Subtract another glyph from the current glyph.
:param other: The :class:`BaseGlyph` instance to subtract
from the current glyph.
:return: A new :class:`BaseGlyph` instance representing
the subtracted results.
.. note::
Subclasses may override this method.
"""
selfMathGlyph = self._toMathGlyph(scaleComponentTransform=True, strict=False)
otherMathGlyph = other._toMathGlyph(scaleComponentTransform=True, strict=False)
result = selfMathGlyph - otherMathGlyph
copied = self._fromMathGlyph(
result, toThisGlyph=False, filterRedundantPoints=True
)
return copied
[docs]
def interpolate(
self,
factor: InterpolationFactorLike,
minGlyph: BaseGlyph,
maxGlyph: BaseGlyph,
round: bool = True,
suppressError: bool = True,
) -> None:
"""Interpolate all possible data in the glyph.
: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 minGlyph: The :class:`BaseGlyph` instance corresponding to the
0.0 position in the interpolation.
:param maxGlyph: The :class:`BaseGlyph` 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:`BaseGlyph`.
Example::
>>> glyph.interpolate(0.5, otherGlyph1, otherGlyph2)
>>> glyph.interpolate((0.5, 2.0), otherGlyph1, otherGlyph2, round=False)
"""
normalizedFactor = normalizers.normalizeInterpolationFactor(factor)
if not isinstance(minGlyph, BaseGlyph):
raise TypeError(
f"Interpolation to an instance of {self.__class__.__name__!r} can not be performed from an instance of {minGlyph.__class__.__name__!r}."
)
if not isinstance(maxGlyph, BaseGlyph):
raise TypeError(
f"Interpolation to an instance of {self.__class__.__name__!r} can not be performed from an instance of {maxGlyph.__class__.__name__!r}."
)
round = normalizers.normalizeBoolean(round)
suppressError = normalizers.normalizeBoolean(suppressError)
self._interpolate(
normalizedFactor,
minGlyph,
maxGlyph,
round=round,
suppressError=suppressError,
)
[docs]
def _interpolate(
self,
factor: InterpolationFactorPair,
minGlyph: BaseGlyph,
maxGlyph: BaseGlyph,
round: bool,
suppressError: bool,
) -> None:
"""Interpolate all possible data in the native glyph.
This is the environment implementation of :meth:`BaseGlyph.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.
"""
setRoundIntegerFunction(normalizers.normalizeVisualRounding)
minMathGlyph = minGlyph._toMathGlyph(scaleComponentTransform=True, strict=False)
maxMathGlyph = maxGlyph._toMathGlyph(scaleComponentTransform=True, strict=False)
try:
result: MathGlyph = interpolate(minMathGlyph, maxMathGlyph, factor)
except IndexError:
result = None
if result is None and not suppressError:
raise FontPartsError(
f"Glyphs '{minGlyph.name}' and '{maxGlyph.name}' could not be interpolated."
)
if result is not None:
if round:
result = result.round()
self._fromMathGlyph(result, toThisGlyph=True, filterRedundantPoints=True)
compatibilityReporterClass = GlyphCompatibilityReporter
@staticmethod
def _checkPairs(
object1: Any, object2: Any, reporter: Any, reporterObject: list[Any]
) -> None:
compatibility = object1.isCompatible(object2)[1]
if compatibility.fatal or compatibility.warning:
if compatibility.fatal:
reporter.fatal = True
if compatibility.warning:
reporter.warning = True
reporterObject.append(compatibility)
[docs]
def isCompatible(
self, other: BaseGlyph, cls=None
) -> tuple[bool, GlyphCompatibilityReporter]:
"""Evaluate interpolation compatibility with another glyph.
:param other: The other :class:`BaseGlyph` 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.GlyphCompatibilityReporter`
instance.
"""
return super().isCompatible(other, BaseGlyph)
[docs]
def _isCompatible(
self, other: BaseGlyph, reporter: GlyphCompatibilityReporter
) -> None:
"""Evaluate interpolation compatibility with another native glyph.
This is the environment implementation of :meth:`BaseGlyph.isCompatible`.
:param other: The other :class:`BaseGlyph` instance to check
compatibility with.
:param reporter: An object used to report compatibility issues.
.. note::
Subclasses may override this method.
"""
GuidelineListType = list[tuple[str | None, int]]
glyph1 = self
glyph2 = other
# contour count
if len(self.contours) != len(glyph2.contours):
reporter.fatal = True
reporter.contourCountDifference = True
# contour pairs
for i in range(min(len(glyph1), len(glyph2))):
contour1 = glyph1[i]
contour2 = glyph2[i]
self._checkPairs(contour1, contour2, reporter, reporter.contours)
# component count
if len(glyph1.components) != len(glyph2.components):
reporter.fatal = True
reporter.componentCountDifference = True
# component check
component_diff: DiffType = []
selfComponents = [component.baseGlyph for component in glyph1.components]
otherComponents = [component.baseGlyph for component in glyph2.components]
for index, (left, right) in enumerate(
zip_longest(selfComponents, otherComponents)
):
if left != right:
component_diff.append((index, left, right))
if component_diff:
reporter.warning = True
reporter.componentDifferences = component_diff
if not reporter.componentCountDifference and set(selfComponents) == set(
otherComponents
):
reporter.componentOrderDifference = True
selfComponents_counted_set = Counter(selfComponents)
otherComponents_counted_set = Counter(otherComponents)
missing_from_glyph1 = (
otherComponents_counted_set - selfComponents_counted_set
)
if missing_from_glyph1:
reporter.fatal = True
reporter.componentsMissingFromGlyph1 = sorted(
missing_from_glyph1.elements()
)
missing_from_glyph2 = (
selfComponents_counted_set - otherComponents_counted_set
)
if missing_from_glyph2:
reporter.fatal = True
reporter.componentsMissingFromGlyph2 = sorted(
missing_from_glyph2.elements()
)
# guideline count
if len(self.guidelines) != len(glyph2.guidelines):
reporter.warning = True
reporter.guidelineCountDifference = True
# guideline check
selfGuidelines: GuidelineListType = []
otherGuidelines: GuidelineListType = []
for source, names in ((self, selfGuidelines), (other, otherGuidelines)):
for i, guideline in enumerate(source.guidelines):
names.append((guideline.name, i))
guidelines1 = set(selfGuidelines)
guidelines2 = set(otherGuidelines)
if len(guidelines1.difference(guidelines2)) != 0:
reporter.warning = True
reporter.guidelinesMissingFromGlyph2 = [
f"{name} {index}"
for name, index in guidelines1.difference(guidelines2)
if name is not None
]
if len(guidelines2.difference(guidelines1)) != 0:
reporter.warning = True
reporter.guidelinesMissingFromGlyph1 = [
f"{name} {index}"
for name, index in guidelines2.difference(guidelines1)
if name is not None
]
# anchor count
if len(self.anchors) != len(glyph2.anchors):
reporter.warning = True
reporter.anchorCountDifference = True
# anchor check
anchor_diff: DiffType = []
selfAnchors = [anchor.name for anchor in glyph1.anchors]
otherAnchors = [anchor.name for anchor in glyph2.anchors]
for index, (left, right) in enumerate(zip_longest(selfAnchors, otherAnchors)):
if left != right:
anchor_diff.append((index, left, right))
if anchor_diff:
reporter.warning = True
reporter.anchorDifferences = anchor_diff
if not reporter.anchorCountDifference and set(selfAnchors) == set(
otherAnchors
):
reporter.anchorOrderDifference = True
selfAnchors_counted_set = Counter(selfAnchors)
otherAnchors_counted_set = Counter(otherAnchors)
missing_from_glyph1 = otherAnchors_counted_set - selfAnchors_counted_set
if missing_from_glyph1:
reporter.anchorsMissingFromGlyph1 = sorted(
missing_from_glyph1.elements()
)
missing_from_glyph2 = selfAnchors_counted_set - otherAnchors_counted_set
if missing_from_glyph2:
reporter.anchorsMissingFromGlyph2 = sorted(
missing_from_glyph2.elements()
)
# ------------
# Data Queries
# ------------
[docs]
def pointInside(self, point: CoordinateLike) -> bool:
"""Check if `point` lies inside the filled area of the glyph.
: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 glyph.
This is the environment implementation of :meth:`BaseGlyph.pointInside`.
: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.
.. note::
Subclasses may override this method.
"""
pen = PointInsidePen(glyphSet=None, testPoint=point, evenOdd=False)
self.draw(pen)
return pen.getResult()
bounds: dynamicProperty = dynamicProperty(
"base_bounds",
"""Get the bounds of the glyph.
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 glyph, or :obj:`None` if the glyph
is empty.
Example::
>>> glyph.bounds
(10, 30, 765, 643)
""",
)
def _get_base_bounds(self) -> BoundingBox | None:
value = self._get_bounds()
if value is not None:
value = normalizers.normalizeBoundingBox(value)
return value
[docs]
def _get_bounds(self) -> BoundingBox | None:
"""Get the bounds of the native glyph.
This is the environment implementation of the :attr:`BaseGlyph.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 glyph, or :obj:`None` if the glyph
is empty.
.. note::
Subclasses may override this method.
"""
pen = BoundsPen(self.layer)
self.draw(pen)
return pen.bounds
area: dynamicProperty = dynamicProperty(
"base_area",
"""Get the area of the glyph
This property is read-only.
:return: An :class:`int` or a :class:` float value representing the
area of the glyph, or :obj:`None` if the glyph is empty.
Example::
>>> glyph.area
583
""",
)
def _get_base_area(self) -> float | None:
value = self._get_area()
if value is not None:
value = normalizers.normalizeArea(value)
return value
def _get_area(self) -> float | None:
"""Get the area of the native glyph
This is the environment implementation of the :attr:`BaseGlyph.area`
property getter.
:return: An :class:`int` or a :class:`float` value representing the
area of the glyph, or :obj:`None` if the glyph is empty.
.. note::
Subclasses may override this method.
"""
pen = AreaPen(self.layer)
self.draw(pen)
return abs(pen.value)
# -----------------
# Layer Interaction
# -----------------
layers: dynamicProperty = dynamicProperty(
"layers",
"""Get the layers of the glyph.
This property is read-only.
:return: A :class:`tuple` of the :class:`BaseLayer` instances belonging
to the glyph.
Example::
>>> glyphLayers = glyph.layers
""",
)
def _get_layers(self, **kwargs) -> tuple[BaseGlyph, ...]:
r"""Get the layers of the native glyph.
:param \**kwargs: Additional keyword arguments.
:return: A :class:`tuple` of the :class:`BaseLayer` subclass instances
belonging to the glyph.
"""
font = self.font
if font is None:
return tuple()
glyphs = []
for layer in font.layers:
if self.name in layer:
glyphs.append(layer[self.name])
return tuple(glyphs)
# get
[docs]
def getLayer(self, name: str) -> BaseGlyph:
"""Get the named layer from the glyph.
:param name: The name of the :class:`BaseLayer` instance to
retrieve.
:return: The specified :class:`BaseLayer` instance.
:raises ValueError: If no layer with the given `name` exists in
the font.
Example::
>>> glyphLayer = glyph.getLayer("foreground")
"""
name = normalizers.normalizeLayerName(name)
return self._getLayer(name)
[docs]
def _getLayer(self, name: str, **kwargs) -> BaseGlyph:
r"""Get the named layer from the native glyph.
:param name: The name of the :class:`BaseLayer` instance to
retrieve.
:param \**kwargs: Additional keyword arguments.
:return: The specified :class:`BaseLayer` instance.
:raises ValueError: If no layer with the given `name` exists in
the font.
.. note::
Subclasses may override this method.
"""
for glyph in self.layers:
if glyph.layer.name == name:
return glyph
raise ValueError(f"No layer named '{name}' in glyph '{self.name}'.")
# new
[docs]
def newLayer(self, name: str) -> BaseGlyph:
"""Create a new layer in the glyph.
If the named layer already exists in the glyph, it
will be cleared.
:param name: The name of the new layer to create.
:return: A newly created :class:`BaseLayer` instance.
Example::
>>> glyphLayer = glyph.newLayer("background")
"""
layerName = name
glyphName = self.name
layerName = normalizers.normalizeLayerName(layerName)
for glyph in self.layers:
if glyph.layer.name == layerName:
layer = glyph.layer
layer.removeGlyph(glyphName)
break
glyph = self._newLayer(name=layerName)
layer = self.font.getLayer(layerName)
return glyph
[docs]
def _newLayer(self, name: str, **kwargs) -> BaseGlyph: # type: ignore[return]
r"""Create a new layer in the glyph.
This is the environment implementation of :meth:`BaseGlyph.newLayer`.
:param name: The name of the new layer to create. The value must
be unique to the font and will have been normalized
with :func:`normalizers.normalizeLayerName`.
:param \**kwargs: Additional keyword arguments.
:return: A newly created :class:`BaseLayer` subclass instance.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# remove
[docs]
def removeLayer(self, layer: BaseGlyph | str) -> None:
"""Remove the specified layer from the glyph.
:param name: The layer to remove as a :class:`BaseLayer` instance,
or a :class:`str` representing the layer name.
Example::
>>> glyph.removeLayer("background")
"""
layerName = layer.layer.name if isinstance(layer, BaseGlyph) else layer
normalizedLayerName = normalizers.normalizeLayerName(layerName)
if self._getLayer(normalizedLayerName).layer.name == normalizedLayerName:
self._removeLayer(normalizedLayerName)
[docs]
def _removeLayer(self, name: str, **kwargs: Any) -> None:
r"""Remove the specified layer from the native glyph.
This is the environment implementation of :meth:`BaseGlyph.removeLayer`.
:param name: The name of the layer to remove. The value 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.
.. note::
Subclasses may override this method.
"""
self.raiseNotImplementedError()
# -----
# Image
# -----
image: dynamicProperty = dynamicProperty(
"base_image",
"""Get the image for the glyph.
This property is read-only.
:return: The :class:`BaseImage` instance belonging to the glyph.
""",
)
def _get_base_image(self) -> BaseImage | None:
image = self._get_image()
if image is not None:
if image.glyph is None:
image.glyph = self
return image
[docs]
def _get_image(self) -> BaseImage | None: # type: ignore[return]
"""Get the image for the native glyph.
:return: The :class:`BaseImage` subclass instance belonging to the glyph,
or :obj:`None` if the glyph has no image.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def addImage(
self,
path: str | None = None,
data: bytes | None = None,
scale: ScaleFactorLike | None = None,
position: CoordinateLike | None = None,
color: RGBALike | None = None,
) -> BaseImage:
"""Set the image in the glyph.
The image data may be provided as either the `path` to an image file or
directly as raw image `data`. The supported image formats will vary
across environments. Refer to :class:`BaseImage` for complete details.
:param path: The optional path to the image file to add to the glyph as
a :class:`str`. Defaults to :obj:`None`.
:param data: The optional raw image data to add to the glyph
as :class:`bytes`. Defaults to :obj:`None`.
:param scale: The optional x and y values to scale the glyph by as
a :class:`tuple` of two :class:`int` or :class:`float` values.
Defaults to :obj:`None`.
:param position: The optional location of the lower left point of the
image as a :ref:`type-coordinate`. Defaults to :obj:`None`.
:param color: The optional color to be applied to the image as
a :ref:`type-color`. Defaults to :obj:`None`.
:return: The :class:`BaseImage` instance added to the glyph.
:raises IOError: If no valid image file can be found at the given path.
:raises FontPartsError: If `path` and `data` are both provided.
Add the image as a filepath::
>>> image = glyph.addImage(path="/path/to/my/image.png")
Add the image as raw data::
>>> image = glyph.addImage(data=someImageData)
Add the image with scale::
>>> image = glyph.addImage(path="/p/t/image.png", scale=(0.5, 1.0))
Add the image with position::
>>> image = glyph.addImage(path="/p/t/image.png", position=(10, 20))
Add the image with color::
>>> image = glyph.addImage(path="/p/t/image.png", color=(1, 0, 0, 0.5))
"""
if path is not None and data is not None:
raise FontPartsError("Only path or data may be defined, not both.")
if scale is None:
scale = (1, 1)
if position is None:
position = (0, 0)
normalizedScale = normalizers.normalizeTransformationScale(scale)
normalizedPosition = normalizers.normalizeTransformationOffset(position)
if color is not None:
normalizedColor = normalizers.normalizeColor(color)
else:
normalizedColor = None
sx, sy = normalizedScale
ox, oy = normalizedPosition
transformation = (sx, 0, 0, sy, ox, oy)
if path is not None:
if not os.path.exists(path):
raise OSError(f"No image located at '{path}'.")
with open(path, "rb") as f:
data = f.read()
if data is not None:
self._addImage(
data=data, transformation=transformation, color=normalizedColor
)
return self.image
[docs]
def _addImage(
self, # type: ignore[return]
data: bytes,
transformation: AffineTransformationLike | None,
color: RGBALike | None,
) -> BaseImage:
"""Set the image in the native glyph.
Each environment may have different possible
formats, so this is unspecified. Assigning the image
to the glyph will be handled by the base class.
:param data: The raw image data to add to the glyph as :class:`bytes`.
:param transformation: The :ref:`type-transformation` values
to be applied to the image or :obj:`None`.
:param color: The color to be applied to the image as
a :ref:`type-color` or :obj:`None`.
:return: The :class:`BaseImage` subclass instance added to the glyph.
:raises NotImplementedError: If the method has not been overridden by a
subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def clearImage(self) -> None:
"""Remove the image from the glyph.
Example::
>>> glyph.clearImage()
"""
if self.image is not None:
self._clearImage()
[docs]
def _clearImage(self, **kwargs: Any) -> None:
r"""Remove the image from the native glyph.
:param \**kwargs: Additional keyword arguments.
:raises NotImplementedError: If the method has not
been overridden by a subclass.
"""
self.raiseNotImplementedError()
# ----------
# Mark color
# ----------
markColor: dynamicProperty = dynamicProperty(
"base_markColor",
"""Get or set the glyph's mark color.
The value must be either a :ref:`type-color` or :obj:`None`.
:return: The :class:`Color` instance assigned to the glyph, or :obj:`None` if
no color has been assigned.
Example::
>>> glyph.markColor
(1, 0, 0, 0.5)
>>> glyph.markColor = None
""",
)
def _get_base_markColor(self) -> Color | None:
value = self._get_markColor()
if value is None:
return None
return Color(value)
def _set_base_markColor(self, value: RGBALike | None) -> None:
if value is not None:
value = normalizers.normalizeColor(value)
self._set_markColor(value)
# type: ignore[return]
[docs]
def _get_markColor(self) -> RGBA | None:
"""Get the glyph's mark color.
This is the environment implementation of
the :attr:`BaseGlyph.markColor` property getter.
:return: The :ref:`type-color` value assigned to the glyph,
or:obj:`None` if no color has been assigned.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def _set_markColor(self, value: RGBALike | None) -> None:
"""Set the glyph's mark color.
This is the environment implementation of
the :attr:`BaseGlyph.markColor` property setter.
:param value: The :ref:`type-color` value to assign to the glyph,
or :obj:`None`.
:raises NotImplementedError: If the method has not been overridden
by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# ----
# Note
# ----
note: dynamicProperty = dynamicProperty(
"base_note",
"""Get or set the glyph's note.
The value must be a :class:`str` or :obj:`None`.
:return: A :class:`str`, or :obj:`None` representing an empty note.
Example::
>>> glyph.note
"P.B. said this looks 'awesome.'"
>>> glyph.note = "P.B. said this looks 'AWESOME.'"
""",
)
def _get_base_note(self) -> str | None:
value = self._get_note()
if value is not None:
value = normalizers.normalizeGlyphNote(value)
return value
def _set_base_note(self, value: str | None) -> None:
if value is not None:
value = normalizers.normalizeGlyphNote(value)
self._set_note(value)
[docs]
def _get_note(self) -> str | None: # type: ignore[return]
"""Get the glyph's note.
This is the environment implementation of the :attr:`BaseGlyph.note`
property getter.
:return: A :class:`str`, or :obj:`None` representing an empty note.
:raises NotImplementedError: If the method has not been overridden
by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def _set_note(self, value: str | None) -> None:
"""Set the glyph's note.
This is the environment implementation of the :attr:`BaseGlyph.note`
property setter.
:param value: The note to assign to the glyph as a :class:`str`
or :obj:`None`.
:raises NotImplementedError: If the method has not been overridden
by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# ---
# Lib
# ---
lib: dynamicProperty = dynamicProperty(
"base_lib",
"""Get the font's lib object.
This property is read-only.
:return: An instance of the :class:`BaseLib` class.
Example::
>>> lib = glyph.lib
""",
)
def _get_base_lib(self) -> BaseLib:
lib = self._get_lib()
lib.glyph = self
return lib
[docs]
def _get_lib(self) -> BaseLib: # type: ignore[return]
"""Get the native glyph's lib object.
This is the environment implementation of :attr:`BaseFont.lib`.
:return: An 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()
# --------
# Temp Lib
# --------
tempLib: dynamicProperty = dynamicProperty(
"base_tempLib",
"""Get the glyph's temporary lib object.
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:`BaseGlyph.lib`, except
that its contents will not be saved when calling
the :meth:`BaseFont.save` method.
This property is read-only.
:return: A temporary instance of the :class:`BaseLib` class.
Example::
>>> tempLib = glyph.tempLib
""",
)
def _get_base_tempLib(self) -> BaseLib:
lib = self._get_tempLib()
lib.glyph = self
return lib
def _get_tempLib(self) -> BaseLib: # type: ignore[return]
"""Get the native glyph's temporary lib object.
This is the environment implementation
of :attr:`BaseGlyph.tempLib`.
: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()
# ---
# API
# ---
def isEmpty(self) -> bool:
"""Check if the glyph is empty.
:return: :obj:`True` if there are no contours and/or
components in the glyph, :obj:`False` otherwise.
Example::
>>> glyph.isEmpty()
.. note:: This method only checks for the presence of contours and
components. Other attributes (guidelines, anchors, a lib, etc.) will
not affect what this method returns.
"""
if self.contours:
return False
if self.components:
return False
return True
def loadFromGLIF(self, glifData: str) -> None:
"""Read data in `GLIF format <http://unifiedfontobject.org/versions/ufo3/glyphs/glif/>`_ into the glyph.
:param glifData: The data to read as a :class:`str`.
Example::
>>> glyph.readGlyphFromString(xmlData)
"""
self._loadFromGLIF(glifData)
def _loadFromGLIF(self, glifData: str) -> None:
"""Read data in `GLIF format <http://unifiedfontobject.org/versions/ufo3/glyphs/glif/>`_ into the native glyph.
:param glifData: The data to read as a :class:`str`.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
def dumpToGLIF(self, glyphFormatVersion: int = 2) -> str:
"""Return the glyph's contents as a string in `GLIF format <http://unifiedfontobject.org/versions/ufo3/glyphs/glif/>`_.
:param glyphFormatVersion: An :class:`int` defining the preferred GLIF
format version.
Example::
>>> xml = glyph.writeGlyphToString()
"""
glyphFormatVersion = normalizers.normalizeGlyphFormatVersion(glyphFormatVersion)
return self._dumpToGLIF(glyphFormatVersion)
def _dumpToGLIF(self, glyphFormatVersion: int) -> str: # type: ignore[return]
"""Return the native glyph's contents as a string in `GLIF format <http://unifiedfontobject.org/versions/ufo3/glyphs/glif/>`_.
:param glyphFormatVersion: An :class:`int` defining the preferred GLIF
format version.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# ---------
# Selection
# ---------
# contours
selectedContours: dynamicProperty = dynamicProperty(
"base_selectedContours",
"""Get or set the selected contours in the glyph.
The value must be a :class:`tuple` or :class:`list` of
either :class:`BaseContour` instances or :class:`int` values
representing contour indexes to select.
:return: A :class:`tuple` of the currently
selected :class:`BaseContour` instances.
Example::
>>> contours = glyph.selectedContours
>>> glyph.selectedContours = otherContours
Set selection using indexes::
>>> glyph.selectedContours = [0, 2]
""",
)
def _get_base_selectedContours(self) -> tuple[BaseContour, ...]:
selected = tuple(
normalizers.normalizeContour(contour)
for contour in self._get_selectedContours()
)
return selected
def _get_selectedContours(self) -> tuple[BaseContour, ...]:
"""Get the selected contours in the native glyph.
This is the environment implementation of
the :attr:`BaseGlyph.selectedContour` property getter.
:return: A :class:`tuple` of the currently selected :class:`BaseContour`
instances. Each value item will be normalized
with :func:`normalizers.normalizeContour`.
.. note::
Subclasses may override this method.
"""
return self._getSelectedSubObjects(self.contours)
def _set_base_selectedContours(
self, value: CollectionType[BaseContour | int]
) -> None:
normalized = []
for contour in value:
normalizedContour: BaseContour | int
if isinstance(contour, int):
normalizedIndex = normalizers.normalizeIndex(contour)
# Avoid mypy conflict with normalizeIndex -> Optional[int]
if normalizedIndex is None: # pragma: no cover
continue
normalizedContour = normalizedIndex
else:
normalizedContour = normalizers.normalizeContour(contour)
normalized.append(normalizedContour)
self._set_selectedContours(normalized)
def _set_selectedContours(self, value: CollectionType[BaseContour | int]) -> None:
"""Set the selected contours in the glyph.
This is the environment implementation of
the :attr:`BaseGlyph.selectedContour` property setter.
:param value: a :class:`tuple` or :class:`list` of
either :class:`BaseContour` instances or :class:`int` values
representing contour indexes to select. Each value item will have
been normalized with :func:`normalizers.normalizeContour`.
or :func:`normalizers.normalizeIndex`.
.. note::
Subclasses may override this method.
"""
return self._setSelectedSubObjects(self.contours, value)
# components
selectedComponents: dynamicProperty = dynamicProperty(
"base_selectedComponents",
"""Get or set the selected components in the glyph.
The value must be a :class:`tuple` or :class:`list` of
either :class:`BaseComponent` instances or :class:`int` values
representing component indexes to select.
:return: A :class:`tuple` of the currently
selected :class:`BaseComponent` instances.
Example::
>>> components = glyph.selectedComponents
>>> glyph.selectedComponents = otherComponents
Set selection using indexes::
>>> glyph.selectedComponents = [0, 2]
""",
)
def _get_base_selectedComponents(self) -> tuple[BaseComponent, ...]:
selected = tuple(
normalizers.normalizeComponent(component)
for component in self._get_selectedComponents()
)
return selected
def _get_selectedComponents(self) -> tuple[BaseComponent, ...]:
"""Get the selected components in the native glyph.
This is the environment implementation of
the :attr:`BaseGlyph.selectedComponents` property getter.
:return: A :class:`tuple` of the currently
selected :class:`BaseComponent` instances. Each value item will be
normalized with :func:`normalizers.normalizeComponent`.
.. note::
Subclasses may override this method.
"""
return self._getSelectedSubObjects(self.components)
def _set_base_selectedComponents(
self, value: CollectionType[BaseComponent | int]
) -> None:
normalized = []
for component in value:
normalizedComponent: BaseComponent | int
if isinstance(component, int):
normalizedIndex = normalizers.normalizeIndex(component)
# Avoid mypy conflict with normalizeIndex -> Optional[int]
if normalizedIndex is None: # pragma: no cover
continue
normalizedComponent = normalizedIndex
else:
normalizedComponent = normalizers.normalizeComponent(component)
normalized.append(normalizedComponent)
self._set_selectedComponents(normalized)
def _set_selectedComponents(
self, value: CollectionType[BaseComponent | int]
) -> None:
"""Set the selected components in the glyph.
This is the environment implementation of
the :attr:`BaseGlyph.selectedComponents` property setter.
:param value: a :class:`tuple` or :class:`list` of
either :class:`BaseComponent` instances or :class:`int` values
representing component indexes to select. Each value item will have
been normalized with :func:`normalizers.normalizeComponent`
or :func:`normalizers.normalizeIndex`.
.. note::
Subclasses may override this method.
"""
return self._setSelectedSubObjects(self.components, value)
# anchors
selectedAnchors: dynamicProperty = dynamicProperty(
"base_selectedAnchors",
"""Get or set the selected anchors in the glyph.
The value must be a :class:`tuple` or :class:`list` of
either :class:`BaseAnchor` instances or :class:`int` values
representing anchor indexes to select.
:return: A :class:`tuple` of the currently
selected :class:`BaseAnchor` instances.
Example::
>>> anchors = glyph.selectedAnchors:
>>> glyph.selectedAnchors = otherAnchors
Set selection using indexes::
>>> glyph.selectedAnchors = [0, 2]
""",
)
def _get_base_selectedAnchors(self) -> tuple[BaseAnchor, ...]:
selected = tuple(
normalizers.normalizeAnchor(anchor)
for anchor in self._get_selectedAnchors()
)
return selected
def _get_selectedAnchors(self) -> tuple[BaseAnchor, ...]:
"""Get the selected anchors in the native glyph.
This is the environment implementation of
the :attr:`BaseGlyph.selectedAnchors` property getter.
:return: A :class:`tuple` of the currently
selected :class:`BaseAnchor` instances. Each value item will be
normalized with :func:`normalizers.normalizeAnchor`.
.. note::
Subclasses may override this method.
"""
return self._getSelectedSubObjects(self.anchors)
def _set_base_selectedAnchors(
self, value: CollectionType[BaseAnchor | int]
) -> None:
normalized = []
for anchor in value:
normalizedAnchor: BaseAnchor | int
if isinstance(anchor, int):
normalizedIndex = normalizers.normalizeIndex(anchor)
# Avoid mypy conflict with normalizeIndex -> Optional[int]
if normalizedIndex is None: # pragma: no cover
continue
normalizedAnchor = normalizedIndex
else:
normalizedAnchor = normalizers.normalizeAnchor(anchor)
normalized.append(normalizedAnchor)
self._set_selectedAnchors(normalized)
def _set_selectedAnchors(self, value: CollectionType[BaseAnchor | int]) -> None:
"""Set the selected anchors in the glyph.
This is the environment implementation of
the :attr:`BaseGlyph.selectedAnchors` property setter.
:param value: a :class:`tuple` or :class:`list` of
either :class:`BaseAnchor` instances or :class:`int` values
representing anchor indexes to select. Each value item will have
been normalized with :func:`normalizers.normalizeAnchor`
or :func:`normalizers.normalizeIndex`.
.. note::
Subclasses may override this method.
"""
return self._setSelectedSubObjects(self.anchors, value)
# guidelines
selectedGuidelines: dynamicProperty = dynamicProperty(
"base_selectedGuidelines",
"""Get or set the selected guidelines in the glyph.
The value must be a :class:`tuple` or :class:`list` of
either :class:`BaseGuideline` instances or :class:`int` values
representing component indexes to select.
:return: A :class:`tuple` of the currently
selected :class:`BaseGuideline` instances.
Example::
>>> guidelines = glyph.selectedGuidelines
>>> glyph.selectedGuidelines = otherGuidelines
Set selection using indexes::
>>> glyph.selectedGuidelines = [0, 2]
""",
)
def _get_base_selectedGuidelines(self) -> tuple[BaseGuideline, ...]:
selected = tuple(
normalizers.normalizeGuideline(guideline)
for guideline in self._get_selectedGuidelines()
)
return selected
def _get_selectedGuidelines(self) -> tuple[BaseGuideline, ...]:
"""Get the selected guidelines in the native glyph.
This is the environment implementation of
the :attr:`BaseGlyph.selectedGuideline` property getter.
:return: A :class:`tuple` of the currently
selected :class:`BaseGuideline` instances. Each value item will be
normalized with :func:`normalizers.normalizeGuideline`.
.. note::
Subclasses may override this method.
"""
return self._getSelectedSubObjects(self.guidelines)
def _set_base_selectedGuidelines(
self, value: CollectionType[BaseGuideline | int]
) -> None:
normalized = []
for guideline in value:
normalizedGuideline: BaseGuideline | int
if isinstance(guideline, int):
normalizedIndex = normalizers.normalizeIndex(guideline)
# Avoid mypy conflict with normalizeIndex -> Optional[int]
if normalizedIndex is None: # pragma: no cover
continue
normalizedGuideline = normalizedIndex
else:
normalizedGuideline = normalizers.normalizeGuideline(guideline)
normalized.append(normalizedGuideline)
self._set_selectedGuidelines(normalized)
def _set_selectedGuidelines(
self, value: CollectionType[BaseGuideline | int]
) -> None:
"""Set the selected guidelines in the glyph.
This is the environment implementation of
the :attr:`BaseGlyph.selectedGuideline` property setter.
:param value: a :class:`tuple` or :class:`list` of
either :class:`BaseGuideline` instances or :class:`int` values
representing guideline indexes to select. Each value item will have
been normalized with :func:`normalizers.normalizeGuideline`
or :func:`normalizers.normalizeIndex`.
.. note::
Subclasses may override this method.
"""
return self._setSelectedSubObjects(self.guidelines, value)