# pylint: disable=C0103, W0613
from __future__ import annotations
import os
from typing import TYPE_CHECKING, Any, Generic, List, Optional, Tuple, Type, Union
from fontTools import ufoLib
from fontParts.base.errors import FontPartsError
from fontParts.base.base import dynamicProperty, InterpolationMixin
from fontParts.base.layer import _BaseGlyphVendor
from fontParts.base import normalizers
from fontParts.base.compatibility import FontCompatibilityReporter
from fontParts.base.deprecated import DeprecatedFont, RemovedFont
from fontParts.base.annotations import (
InterpolationFactorPair,
InterpolationFactorLike,
RGBALike,
RGBA,
Coordinate,
CoordinateLike,
CharacterMappingType,
CollectionType,
IntFloatType,
KerningDictType,
ReverseComponentMappingType,
)
if TYPE_CHECKING:
from fontParts.base.info import BaseInfo
from fontParts.base.groups import BaseGroups
from fontParts.base.kerning import BaseKerning
from fontParts.base.features import BaseFeatures
from fontParts.base.lib import BaseLib
from fontParts.base.layer import BaseLayer
from fontParts.base.glyph import BaseGlyph
from fontParts.base.guideline import BaseGuideline
[docs]
class BaseFont(_BaseGlyphVendor, InterpolationMixin, DeprecatedFont, RemovedFont):
"""Represent the basis for a font object.
Instances of this class are almost always created with one of the
font functions in :ref:`fontParts.world`.
This class will be instantiated in different ways depending on
the value type of the `pathOrObject` parameter.
:param pathOrObject: The source for initializing the font.
If :obj:`None`, a new, empty font will be created. If
a :class:`str` representing the path to an existing file,
the class will open and read the file at this path. If an
instance of the environment's unwrapped native font object,
it will be wrapped with FontParts. Defaults to :obj:`None`.
:param showInterface: Whether to display the graphical
interface. Defaults to :obj:`True`.
"""
def __init__(
self, pathOrObject: str | BaseFont | None = None, showInterface: bool = True
) -> None:
super().__init__(pathOrObject=pathOrObject, showInterface=showInterface)
def _reprContents(self) -> list[str]:
contents: list[str] = [f"'{self.info.familyName} {self.info.styleName}'"]
if self.path is not None:
contents.append(f"path={self.path!r}")
return contents
# ----
# Copy
# ----
copyAttributes: tuple[str, ...] = (
"info",
"groups",
"kerning",
"features",
"lib",
"layerOrder",
"defaultLayerName",
"glyphOrder",
)
[docs]
def copy(self) -> BaseFont:
"""Copy data from the current font into a new font.
This will copy:
- :attr:`~BaseFont.info`
- :attr:`~BaseFont.groups`
- :attr:`~BaseFont.kerning`
- :attr:`~BaseFont.features`
- :attr:`~BaseFont.lib`
- :attr:`~BaseFont.layers`
- :attr:`~BaseFont.layerOrder`
- :attr:`~BaseFont.defaultLayerName`
- :attr:`~BaseFont.glyphOrder`
- :attr:`~BaseFont.guidelines`
:return: A new :class:`BaseFont` instance with the same
attributes as the current instance.
Example::
>>> copiedFont = font.copy()
"""
return super().copy()
def copyData(self, source: BaseFont) -> None:
"""Copy data from another font instance.
Refer to :meth:`BaseFont.copy` for a list of values that will be
copied.
:param source: The source :class:`BaseFont` instance from which
to copy data.
Example::
>>> sourceFont = MyFont('path/to/source.ufo')
>>> font.copyData(sourceFont)
"""
# set the default layer name
self.defaultLayer.name = source.defaultLayerName
for layerName in source.layerOrder:
if layerName in self.layerOrder:
layer = self.getLayer(layerName)
else:
layer = self.newLayer(layerName)
layer.copyData(source.getLayer(layerName))
for guideline in source.guidelines:
self.appendGuideline(guideline=guideline)
super().copyData(source)
# ---------------
# File Operations
# ---------------
# Initialize
[docs]
def _init(
self, pathOrObject: str | BaseFont | None, showInterface: bool, **kwargs: Any
) -> None:
r"""Initialize the native font object.
This method is the environment implementation
of :meth:`BaseFont.__init__`. It will wrap a native font
object based on the value type of the `pathOrObject` parameter.
:param pathOrObject: The source for initializing the font. Options are:
+--------------------+---------------------------------------------------+
| Type | Description |
+--------------------+---------------------------------------------------+
| :obj:`None` | Create a new font. |
| :class:`str` | Open the font file located at the given location. |
| native font object | Wrap the given object. |
+--------------------+---------------------------------------------------+
:param showInterface: Whether to display the graphical
interface.
: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()
# path
path: dynamicProperty = dynamicProperty(
"base_path",
"""Get the path to the font file.
This property is read-only.
:return: A :class:`str` defining the location of the file
or :obj:`None` to indicate that the font does not have a
file representation.
Example::
>>> print(font.path)
"/path/to/my/font.ufo"
""",
)
def _get_base_path(self) -> str | None:
path: str | None = self._get_path()
if path is not None:
path = normalizers.normalizeFilePath(path)
return path
[docs]
def _get_path(self, **kwargs: Any) -> str | None: # type: ignore[return]
r"""Get the path to the native font file.
This method is the environment implementation
of :attr:`BaseFont.path`.
:param \**kwargs: Additional keyword arguments.
:return: A :class:`str` defining the location of the file
or :obj:`None` to indicate that the font does not have a
file representation. If the value is not :obj:`None` it will
be normalized with :func:`normalizers.normalizeFilePath`.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# save
[docs]
def save(
self,
path: str | None = None,
showProgress: bool = False,
formatVersion: int | None = None,
fileStructure: str | None = None,
) -> None:
"""Save the font to the specified path.
:param path: The path to which the font should be saved.
If :obj:`None`, the font is saved to its original location.
The file type is inferred from the file extension of the
path. If no extension is given, the environment may use a
default format. Defaults to :obj:`None`.
:param showProgress: Whether to display a progress bar during
the operation. Environments may or may not implement this
behavior. Defaults to :obj:`False`.
:param formatVersion: The format version to use when saving the
file. For example, a `formatVersion` of 2 will save the file
in UFO 2 format. If :obj:`None`, the original format version
will be preserved, or the latest version supported by the
environment will be used if no original version exists.
Defaults to :obj:`None`.
:param fileStructure: The file structure for saving UFO formats.
Can be :obj:`None`, which uses the existing file structure
or the default structure for unsaved fonts; ``'package'``,
which is the default structure; or ``'zip'``, which saves
the font as a ``.ufoz`` file. Defaults to :obj:`None`.
:raises IOError: If no file location is given in either the
`path` parameter or the :attr:`BaseFont.path` attribute.
.. note::
Environments may define their own rules regarding when a
file should be saved to its original location versus a new
location. For example, a font opened from a compiled
OpenType font may not be saved back into the original
OpenType file.
Example::
>>> font.save()
>>> font.save("/path/to/my/font-2.ufo")
"""
if path is None and self.path is None:
raise OSError(
"The font cannot be saved because no file location has been given."
)
if path is not None:
path = normalizers.normalizeFilePath(path)
showProgress = bool(showProgress)
if formatVersion is not None:
formatVersion = normalizers.normalizeFileFormatVersion(formatVersion)
if fileStructure is not None:
fileStructure = normalizers.normalizeFileStructure(fileStructure)
self._save(
path=path,
showProgress=showProgress,
formatVersion=formatVersion,
fileStructure=fileStructure,
)
[docs]
def _save(
self,
path: str | None,
showProgress: bool,
formatVersion: int | None,
fileStructure: str | None,
**kwargs: Any,
) -> None:
r"""Save the native font to the specified path.
This is the environment implementation of :meth:`BaseFont.save`.
:param path: The file path to save the data to. If not :obj:`None`, the value
will have been normalized with :func:`normalizers.normalizeFilePath`.
:param showProgress: Whether to display a progress bar during the
operation. Environments are not required to display a progress bar
even if value is :obj:`True`.
:param formatVersion: The file format version to write the data into.
If not :obj:`None`, the value will have been normalized
with :func:`normalizers.normalizeFileFormatVersion`.
:param fileStructure: The file structure to use.
: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()
# close
[docs]
def close(self, save: bool = False) -> None:
"""Close the font.
:param save: Whether to save the font before closing.
Defaults to :obj:`False`
Example::
>>> font.close()
>>> font.close(save=True)
"""
if save:
self.save()
self._close()
[docs]
def _close(self, **kwargs: Any) -> None:
r"""Close the native font.
This is the environment implementation
of :meth:`BaseFont.close`.
: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()
# generate
@staticmethod
def generateFormatToExtension(format: str, fallbackFormat: str) -> str:
"""Return the file extension for the given format identifier.
This method maps format identifiers to file extensions. If the
provided `format` is not in the map, the method returns the
`fallbackFormat`.
:param format: The format identifier to map to a file extension. Options are:
+--------------------+-------------------------------------------------------+
| Format | Description |
+--------------------+-------------------------------------------------------+
| ``'mactype1'`` | Mac Type 1 font (generates suitcase and LWFN file) |
| ``'macttf'`` | Mac TrueType font (generates suitcase) |
| ``'macttdfont'`` | Mac TrueType font (generates suitcase with data fork) |
| ``'otfcff'`` | PS OpenType (CFF-based) font (OTF) |
| ``'otfttf'`` | PC TrueType/TT OpenType font (TTF) |
| ``'pctype1'`` | PC Type 1 font (binary/PFB) |
| ``'pcmm'`` | PC MultipleMaster font (PFB) |
| ``'pctype1ascii'`` | PC Type 1 font (ASCII/PFA) |
| ``'pcmmascii'`` | PC MultipleMaster font (ASCII/PFA) |
| ``'ufo1'`` | UFO format version 1 |
| ``'ufo2'`` | UFO format version 2 |
| ``'ufo3'`` | UFO format version 3 |
| ``'unixascii'`` | UNIX ASCII font (ASCII/PFA) |
+--------------------+-------------------------------------------------------+
:param fallbackFormat: The extension to return if `format` is
unrecognized.
:return: The corresponding file extension for the `format`
identifier or the `fallbackFormat` if the format is
unrecognized.
"""
formatToExtension = dict(
# mactype1=None,
macttf=".ttf",
macttdfont=".dfont",
otfcff=".otf",
otfttf=".ttf",
# pctype1=None,
# pcmm=None,
# pctype1ascii=None,
# pcmmascii=None,
ufo1=".ufo",
ufo2=".ufo",
ufo3=".ufo",
unixascii=".pfa",
)
return formatToExtension.get(format, fallbackFormat)
[docs]
def generate(
self, format: str, path: str | None = None, **environmentOptions: Any
) -> None:
r"""Generate the font in another format.
This method converts the font to the specified format and saves
it to the specified path. Standard format identifiers can be
found in :attr:`BaseFont.generateFormatToExtension`.
Environments may support additional keyword arguments in this
method. For example, if the tool allows decomposing components
during generation, this can be specified with an additional
keyword argument.
:param format: The file format identifier for the output.
:param path: The location to save the generated file. If not
provided, the file will be saved in the same directory as
the source font, with the current file name and the
appropriate suffix for the format. If a directory is
specified, the file will be saved with the current file
name and the appropriate suffix for the format. If a file
already exists at that location, it will be overwritten.
:param \**environmentOptions: Additional keyword arguments for
environment-specific options.
:raises ValueError: If `format` is not defined.
:raises TypeError: If `format` is not a :class:`str`.
:raises UserWarning: If an unsupported environment option is
passed.
:raises IOError: If the output path is not defined and the
source font does not have a path.
Example::
>>> font.generate("otfcff")
>>> font.generate("otfcff", "/path/to/my/font.otf")
"""
import warnings
if format is None:
raise ValueError("The format must be defined when generating.")
elif not isinstance(format, str):
raise TypeError("The format must be defined as a string.")
env = {}
for key, value in environmentOptions.items():
valid = self._isValidGenerateEnvironmentOption(key)
if not valid:
warnings.warn(
f"The {key} argument is not supported in this environment.",
UserWarning,
)
env[key] = value
environmentOptions = env
ext = self.generateFormatToExtension(format, "." + format)
if path is None and self.path is None:
raise OSError(
"The file cannot be generated because an output path was not defined."
)
elif path is None:
path = os.path.splitext(self.path)[0]
path += ext
elif os.path.isdir(path):
if self.path is None:
raise OSError(
"The file cannot be generated because "
"the file does not have a path."
)
fileName = os.path.basename(self.path)
fileName += ext
path = os.path.join(path, fileName)
path = normalizers.normalizeFilePath(path)
return self._generate(
format=format, path=path, environmentOptions=environmentOptions
)
[docs]
@staticmethod
def _isValidGenerateEnvironmentOption(name: str) -> bool:
"""Validate if the environment option is supported.
Any unknown keyword arguments given to :meth:`BaseFont.generate`
are passed to this method. `name` is the name used for the
argument. Environments may evaluate if `name` is a supported
option.
:param name: The name of the environment option to validate.
:return: :obj:`True` if the environment option is supported,
otherwise :obj:`False`.
.. note::
Subclasses may override this method.
"""
return False
[docs]
def _generate(
self, format: str, path: str | None, environmentOptions: dict, **kwargs: object
) -> None:
"""Generate the native font in another format.
This is the environment implementation
of :meth:`BaseFont.generate`. Refer to
the :attr:`BaseFont.generateFormatToExtension` documentation
for the standard format identifiers.
:param format: The output format identifier. If the value given
for `format` is not supported by the environment,
the environment must raise :exc:`FontPartsError`.
:param path: The location where the generated file should be
saved. The value will have been normalized
with :func:`normalizers.normalizeFilePath`.
:param environmentOptions: A dictionary of environment-specific
options. These options are validated
with :meth:`BaseFont._isValidGenerateEnvironmentOption` and
the given values. These values are not passed through any
normalization functions.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# -----------
# Sub-Objects
# -----------
# info
info: dynamicProperty = dynamicProperty(
"base_info",
"""Get the font's info object.
This property is read-only.
:return: An instance of the :class:`BaseInfo` class.
Example::
>>> font.info.familyName
"My Family"
""",
)
def _get_base_info(self) -> BaseInfo:
info: BaseInfo = self._get_info()
info.font = self
return info
[docs]
def _get_info(self) -> BaseInfo: # type: ignore[return]
"""Get the native font's info object.
This is the environment implementation of :attr:`BaseFont.info`.
:return: An instance of a :class:`BaseInfo` subclass.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# groups
groups: dynamicProperty = dynamicProperty(
"base_groups",
"""Get the font's groups object.
This property is read-only.
:return: An instance of the :class:`BaseGroups` class.
Example::
>>> font.groups["myGroup"]
["A", "B", "C"]
""",
)
def _get_base_groups(self) -> BaseGroups:
groups: BaseGroups = self._get_groups()
groups.font = self
return groups
[docs]
def _get_groups(self) -> BaseGroups: # type: ignore[return]
"""Get the native font's groups object.
This is the environment implementation
of :attr:`BaseFont.groups`.
:return: an instance of a :class:`BaseGroups` subclass.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# kerning
kerning: dynamicProperty = dynamicProperty(
"base_kerning",
"""Get the font's kerning object.
This property is read-only.
:return: An instance of the :class:`BaseKerning` class.
Example::
>>> font.kerning["A", "B"]
-100
""",
)
def _get_base_kerning(self) -> BaseKerning:
kerning: BaseKerning = self._get_kerning()
kerning.font = self
return kerning
[docs]
def _get_kerning(self) -> BaseKerning: # type: ignore[return]
"""Get the native font's kerning object.
This is the environment implementation
of :attr:`BaseFont.kerning`.
:return: An instance of a :class:`BaseKerning` subclass.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def getFlatKerning(self) -> KerningDictType:
"""Get the font's kerning as a flat dictionary.
:return: A :class:`dict` of the font's :class:`BaseKerning` keys
mapped to their respective values.
"""
return self._getFlatKerning()
[docs]
def _getFlatKerning(self) -> KerningDictType:
"""Get the native font's kerning as a flat dictionary.
This is the environment implementation of
:meth:`BaseFont.getFlatKerning`.
:return: A :class:`dict` of the font's :class:`BaseKerning`
subclass keys mapped to their respective values.
.. note::
Subclasses may override this method.
"""
kernOrder = {
(True, True): 0, # group group
(True, False): 1, # group glyph
(False, True): 2, # glyph group
(False, False): 3, # glyph glyph
}
def kerningSortKeyFunc(pair):
g1, g2 = pair
g1grp = g1.startswith("public.kern1.")
g2grp = g2.startswith("public.kern2.")
return (kernOrder[g1grp, g2grp], pair)
flatKerning = dict()
kerning = self.kerning
groups = self.groups
for pair in sorted(self.kerning.keys(), key=kerningSortKeyFunc):
kern_value = kerning[pair]
(left, right) = pair
if left.startswith("public.kern1."):
left = groups.get(left, [])
else:
left = [left]
if right.startswith("public.kern2."):
right = groups.get(right, [])
else:
right = [right]
for right_glyph in right:
for left_glyph in left:
flatKerning[(left_glyph, right_glyph)] = kern_value
return flatKerning
# features
features: dynamicProperty = dynamicProperty(
"base_features",
"""Get the font's features object.
This property is read-only.
:return: An instance of the :class:`BaseFeatures` class.
Example::
>>> font.features.text
"include(features/substitutions.fea);"
""",
)
def _get_base_features(self) -> BaseFeatures:
features: BaseFeatures = self._get_features()
features.font = self
return features
[docs]
def _get_features(self) -> BaseFeatures: # type: ignore[return]
"""Get the native font's features object.
This is the environment implementation of
:attr:`BaseFont.features`.
:return: An instance of a :class:`BaseFeatures` subclass.
: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::
>>> font.lib["org.robofab.hello"]
"world"
""",
)
def _get_base_lib(self) -> BaseLib:
lib: BaseLib = self._get_lib()
lib.font = self
return lib
[docs]
def _get_lib(self) -> BaseLib: # type: ignore[return]
"""Get the native font'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()
# tempLib
tempLib: dynamicProperty = dynamicProperty(
"base_tempLib",
"""Get the font's temporary lib object.
This property is read-only.
This property provides access to a temporary instance of
the :class:`BaseLib` class, used for storing data that should
not be persisted. It is similar to :attr:`BaseFont.lib`, except
that its contents will not be saved when calling
the :meth:`BaseFont.save` method.
:return: A temporary instance of the :class:`BaseLib` class.
Example::
>>> font.tempLib["org.robofab.hello"]
"world"
""",
)
def _get_base_tempLib(self) -> BaseLib:
lib: BaseLib = self._get_tempLib()
lib.font = self
return lib
[docs]
def _get_tempLib(self) -> BaseLib: # type: ignore[return]
"""Get the native font's temporary lib object.
This is the environment implementation
of :attr:`BaseFont.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()
# -----------------
# Layer Interaction
# -----------------
layers: dynamicProperty = dynamicProperty(
"base_layers",
"""Get the font's layer objects.
This property is read-only.
:return: A :class:`tuple` containing instances of
the :class:`BaseLayer` class.
Example::
>>> for layer in font.layers:
... layer.name
"My Layer 1"
"My Layer 2"
""",
)
def _get_base_layers(self) -> tuple[BaseLayer, ...]:
layers: tuple[BaseLayer, ...] = self._get_layers()
for layer in layers:
self._setFontInLayer(layer)
return tuple(layers)
[docs]
def _get_layers(self, **kwargs: Any) -> tuple[BaseLayer, ...]: # type: ignore[return]
r"""Get the native font's layer objects.
This is the environment implementation of
:attr:`BaseFont.layers`.
:param \**kwargs: Additional keyword arguments.
:return: A :class:`tuple` containing instances
of the :class:`BaseLayer` subclass. The items should be in
the order defined by :attr:`BaseFont.layerOrder`.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# order
layerOrder: dynamicProperty = dynamicProperty(
"base_layerOrder",
"""Get or set the order of the layers in the font.
The value must be a :class:`list` or :class:`tuple` of layer
names as :class:`str` reflecting the desired order of the
font's :class:`BaseLayer` objects.
:return: A :class:`tuple` of layer names as :class:`str` in
their defined order.
Example::
>>> font.layerOrder = ["My Layer 2", "My Layer 1"]
>>> font.layerOrder
("My Layer 2", "My Layer 1")
""",
)
def _get_base_layerOrder(self) -> tuple[str, ...]:
value: CollectionType[str] = self._get_layerOrder()
value = normalizers.normalizeLayerOrder(value, self)
return value
def _set_base_layerOrder(self, value: CollectionType[str]) -> None:
value = normalizers.normalizeLayerOrder(value, self)
self._set_layerOrder(value)
[docs]
def _get_layerOrder(self, **kwargs: Any) -> tuple[str, ...]: # type: ignore[return]
r"""Get the order of the layers in the native font.
This is the environment implementation of the
:attr:`BaseFont.layerOrder` property getter.
:param \**kwargs: Additional keyword arguments.
:return: A :class:`list` of layer names in their defined order.
The value will be normalized with :func:`normalizers.normalizeLayerOrder`.
:raises NotImplementedError: If the method has not
beenoverridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def _set_layerOrder(self, value: CollectionType[str], **kwargs: Any) -> None:
r"""Set the order of the layers in the native font.
This is the environment implementation of the
:attr:`BaseFont.layerOrder` property setter.
:param value: A :class:`list` or :class:`tuple` of layer names
reflecting the desired order of the font's :class:`BaseLayer` objects.
The value will have been normalized
with :func:`normalizers.normalizeLayerOrder`.
: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()
# default layer
def _setFontInLayer(self, layer: BaseLayer) -> None:
if layer.font is None:
layer.font = self
defaultLayerName: dynamicProperty = dynamicProperty(
"base_defaultLayerName",
"""Get or set the name of the font's default layer.
The value must be name of the desired default :class`BaseLayer`
instance as a :class:`str`.
:return: The name of the current default :class`BaseLayer`
instance.
Example::
>>> font.defaultLayerName = "My Layer 2"
>>> font.defaultLayerName
"My Layer 2"
""",
)
def _get_base_defaultLayerName(self) -> str:
value = self._get_defaultLayerName()
value = normalizers.normalizeDefaultLayerName(value, self)
return value
def _set_base_defaultLayerName(self, value: str) -> None:
value = normalizers.normalizeDefaultLayerName(value, self)
self._set_defaultLayerName(value)
[docs]
def _get_defaultLayerName(self) -> str: # type: ignore[return]
"""Get the name of the native font's default layer.
This is the environment implementation of
:attr:`BaseFont.defaultLayerName` property getter.
:return: The name of the current default :class`BaseLayer`
subclass instance. The value will be normalized
with :func:`normalizers.normalizeDefaultLayerName`.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def _set_defaultLayerName(self, value: str, **kwargs: Any) -> None:
r"""Set the name of the native font's default layer.
This is the environment implementation of
:attr:`BaseFont.defaultLayerName` property setter.
:param value: The name of the desired default :class`BaseLayer`
subclass instance. The name will have been normalized
with :func:`normalizers.normalizeDefaultLayerName`.
: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()
defaultLayer: dynamicProperty = dynamicProperty(
"base_defaultLayer",
"""Get or set the font's default layer.
The value must be the desired default :class:`BaseLayer` instance.
:return: The current default :class:`BaseLayer` instance.
Example::
>>> layer = font.defaultLayer
>>> font.defaultLayer = otherLayer
""",
)
def _get_base_defaultLayer(self) -> BaseLayer:
layer: BaseLayer = self._get_defaultLayer()
layer = normalizers.normalizeLayer(layer)
return layer
def _set_base_defaultLayer(self, layer: BaseLayer) -> None:
layer = normalizers.normalizeLayer(layer)
self._set_defaultLayer(layer)
[docs]
def _get_defaultLayer(self) -> BaseLayer:
"""Get the native font's default layer.
This is the environment implementation of the
:attr:`BaseFont.defaultLayer` property getter.
:return: The default :class:`BaseLayer` subclass instance.
The value will be normalized with :func:`normalizers.normalizeLayer`.
.. important::
Subclasses must override this method.
"""
name = self.defaultLayerName
layer = self.getLayer(name)
return layer
[docs]
def _set_defaultLayer(self, value: BaseLayer) -> None:
"""Set the native font's default layer.
This is the environment implementation of the
:attr:`BaseFont.defaultLayer` property setter.
:param value: The desired default :class:`BaseLayer` subclass
instance. The value will have been normalized
with :func:`normalizers.normalizeLayer`.
.. important::
Subclasses must override this method.
"""
self.defaultLayerName = value.name
# get
[docs]
def getLayer(self, name: str) -> BaseLayer:
"""Get the named layer from the font.
: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::
>>> font.getLayer("My Layer 2")
<Layer 'My Layer 2' at 0x...>
"""
name = normalizers.normalizeLayerName(name)
if name not in self.layerOrder:
raise ValueError(f"No layer with the name '{name}' exists.")
layer = self._getLayer(name)
self._setFontInLayer(layer)
return layer
[docs]
def _getLayer(self, name: str, **kwargs: Any) -> BaseLayer:
r"""Get the named layer from the native font.
This is the environment implementation of
:meth:`BaseFont.getLayer`.
:param name: The name of the :class:`BaseLayer` subclass
instance to retrieve.
:param \**kwargs: Additional keyword arguments.
:return: The specified :class:`BaseLayer` subclass instance. The value
will have been normalized with :func:`normalizers.normalizeLayerName`
and verified as an existing layer.
:raises ValueError: If no layer with the given `name` exists in
the font.
.. note::
Subclasses may override this method.
"""
for layer in self.layers:
if layer.name == name:
return layer
raise ValueError(f"No layer with the name '{name}' exists.")
# new
[docs]
def newLayer(self, name: str, color: RGBALike | None = None) -> BaseLayer:
"""Create a new layer in the font.
:param name: The name of the new layer to create.
:param color: The color value to assign to the new layer.
Defaults to :obj:`None`.
:return: A newly created :class:`BaseLayer` instance.
Example::
>>> layer = font.newLayer("My Layer 3")
"""
name = normalizers.normalizeLayerName(name)
if name in self.layerOrder:
layer = self.getLayer(name)
if color is not None:
layer.color = color
return layer
if color is not None:
color = normalizers.normalizeColor(color)
layer = self._newLayer(name=name, color=color)
self._setFontInLayer(layer)
return layer
[docs]
def _newLayer( # type: ignore[return]
self, name: str, color: RGBALike | None, **kwargs: Any
) -> BaseLayer:
r"""Create a new layer in the native font.
This is the environment implementation of
:meth:`BaseFont.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 color: The color value to assign to the new layer. If the
value is not :obj:`None`, it will have been normalized with
:func:`normalizers.normalizeColor`.
: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, name: str) -> None:
"""Remove the specified layer from the font.
:param name: The name of the layer to remove.
:raises ValueError: If no layer with the given `name` exists in
the font.
Example::
>>> font.removeLayer("My Layer 3")
"""
name = normalizers.normalizeLayerName(name)
if name not in self.layerOrder:
raise ValueError(f"No layer with the name '{name}' exists.")
self._removeLayer(name)
[docs]
def _removeLayer(self, name: str, **kwargs: Any) -> None:
r"""Remove the specified layer from the native font.
This is the environment implementation of
:meth:`BaseFont.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.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
# insert
[docs]
def insertLayer(self, layer: BaseLayer, name: str | None = None) -> BaseLayer:
"""Insert a specified layer into the font.
This method will not insert a layer directly, but rather create
a new :class:`BaseLayer` instance containing the data from
`layer`. The data inserted from `layer` is the same data as
documented in :meth:`BaseLayer.copy`.
:param layer: The :class:`BaseLayer` instance to insert.
:param name: The name to assign to the new layer after
insertion. If value is :obj:`None`, the origninal name will
be used. Defaults to :obj:`None`.
:return: The newly inserted :class:`BaseLayer` instance.
Example::
>>> layer = font.insertLayer(otherLayer, name="layer 2")
"""
if name is None:
name = layer.name
normalizedName = normalizers.normalizeLayerName(name)
if normalizedName in self:
self.removeLayer(normalizedName)
return self._insertLayer(layer, name=normalizedName)
[docs]
def _insertLayer(self, layer: BaseLayer, name: str, **kwargs: Any) -> BaseLayer:
r"""Insert a specified layer into the native font.
This is the environment implementation of :meth:`BaseFont.insertLayer`.
An environment must not insert `layer` directly, but rather copy
it's data to a new layer.
:param layer: A layer object with the attributes necessary for
copying as defined in :meth:`BaseLayer.copy`.
:param name: The name to assign to the new layer after
insertion. The value will have been normalized
with :func:`normalizers.normalizeLayerName` and tested to
make sure that it is unique to the font.
:param \**kwargs: Additional keyword arguments.
:return: The newly inserted :class:`BaseLayer` subclass instance.
.. note::
Subclasses may override this method.
"""
if name != layer.name and layer.name in self.layerOrder:
layer = layer.copy()
layer.name = name
dest = self.newLayer(name)
dest.copyData(layer)
return dest
# duplicate
[docs]
def duplicateLayer(self, layerName: str, newLayerName: str) -> BaseLayer:
"""Duplicate the specified layer in the font.
This method creates a new :class:`BaseLayer` instance. It copies
data from the layer named `layerName` into this new instance,
assigns it the name specified by `newLayerName`, and then
inserts the new layer into the font.
:param layerName: The name of the layer to duplicate.
:param newLayerName: The new name to assign to the duplicated
layer.
:return: The newly duplicated :class:`BaseLayer` instance.
:raises ValueError: If no layer with the given `name` exists in
the font.
Example::
>>> layer = font.duplicateLayer("layer 1", "layer 2")
"""
layerOrder = self.layerOrder
layerName = normalizers.normalizeLayerName(layerName)
if layerName not in layerOrder:
raise ValueError(f"No layer with the name '{layerName}' exists.")
newLayerName = normalizers.normalizeLayerName(newLayerName)
if newLayerName in layerOrder:
raise ValueError(f"A layer with the name '{newLayerName}' already exists.")
newLayer = self._duplicateLayer(layerName, newLayerName)
newLayer = normalizers.normalizeLayer(newLayer)
return newLayer
[docs]
def _duplicateLayer(self, layerName: str, newLayerName: str) -> BaseLayer:
"""Duplicate the specified layer in the native font.
This is the environment implementation
of :meth:`BaseFont.duplicateLayer`.
:param layerName: The name of the layer to duplicate. The value
will have been normalized
with :func:`normalizers.normalizeLayerName` and tested to
make sure that it already exists in the font.
:param newLayerName: The new name to assign to the duplicated
layer. The value will have been normalized
with :func:`normalizers.normalizeLayerName` and tested to
make sure that it does not already exist in the font.
:return: The newly duplicated :class:`BaseLayer` subclass
instance.
.. note::
Subclasses may override this method.
"""
newLayer = self.getLayer(layerName).copy()
return self.insertLayer(newLayer, newLayerName)
[docs]
def swapLayerNames(self, layerName: str, otherLayerName: str) -> None:
"""Swap the names of two specific layers in the font.
This method assigns the name `layerName` to the layer currently
named `otherLayerName` and assigns the name `otherLayerName` to
the layer currently named `layerName`.
:param layerName: The name of one layer.
:param otherNAme: The name of the other layer.
:raises ValueError: If no layer with the given `layerName` or
`otherLayerName` exists in the font.
Example::
>>> font.swapLayerNames("before drawing revisions",
... "after drawing revisions")
"""
layerOrder = self.layerOrder
layerName = normalizers.normalizeLayerName(layerName)
if layerName not in layerOrder:
raise ValueError(f"No layer with the name '{layerName}' exists.")
otherLayerName = normalizers.normalizeLayerName(otherLayerName)
if otherLayerName not in layerOrder:
raise ValueError(f"No layer with the name '{otherLayerName}' exists.")
self._swapLayerNames(layerName, otherLayerName)
def _swapLayerNames(self, layerName: str, otherLayerName: str) -> None:
"""Swap the names of two specific layers in the native font.
This is the environment implementation
of :meth:`BaseFont.swapLayerNames`.
Both `layerName` and `otherLayerName` will have been normalized
with :func:`normalizers.normalizeLayerName` and tested to make
sure they already exist in the font.
:param layerName: The name of one layer.
:param otherName: The name of the other layer.
.. note::
Subclasses may override this method.
"""
import random
layer1 = self.getLayer(layerName)
layer2 = self.getLayer(otherLayerName)
# make a temporary name and assign it to
# the first layer to prevent two layers
# from having the same name at once.
layerOrder = self.layerOrder
for _ in range(50):
# shout out to PostScript unique IDs
tempLayerName = str(random.randint(4000000, 4999999))
if tempLayerName not in layerOrder:
break
else:
raise FontPartsError(
"Couldn't find a temporary layer name after 50 tries. "
"Sorry. Please try again."
)
layer1.name = tempLayerName
# now swap
layer2.name = layerName
layer1.name = otherLayerName
# -----------------
# Glyph Interaction
# -----------------
# base implementation overrides
[docs]
def _getItem(self, name: str, **kwargs: Any) -> BaseGlyph:
r"""Get the specified glyph from the native default layer.
This is the environment implementation of
:meth:`BaseFont.__getitem__`.
:param name: The name of the glyph to retrieve from the default layer.
The value will have been normalized
with :func:`normalizers.normalizeGlyphName`.
:param \**kwargs: Additional keyword arguments.
:return: the specified instance of a :class:`BaseGlyph`
subclass.
.. note::
Subclasses may override this method.
"""
layer = self.defaultLayer
return layer[name]
[docs]
def _keys(self, **kwargs: Any) -> tuple[str, ...]:
r"""Get a list of all glyph names in the native default layer.
This is the environment implementation of :meth:`BaseFont.keys`.
:param \**kwargs: Additional keyword arguments.
:return: A :class:`tuple` of glyph names as :class:`str`.
.. note::
Subclasses may override this method.
"""
layer = self.defaultLayer
return layer.keys()
[docs]
def _newGlyph(self, name: str, **kwargs: Any) -> BaseGlyph:
r"""Create a new glyph in the native default layer.
This is the environment implementation of :meth:`BaseFont.newGlyph.
:param name: The name to assign to the new glyph. The value will
have been normalized
with :func:`normalizers.normalizeGlyphName` and verified as
unique within the default layer.
:param \**kwargs: Additional keyword arguments.
:return: An instance of a :class:`BaseGlyph` subclass representing
the new glyph.
.. note::
Subclasses may override this method.
"""
layer = self.defaultLayer
# clear is False here because the base newFont
# that has called this method will have already
# handled the clearing as specified by the caller.
return layer.newGlyph(name, clear=False)
[docs]
def _removeGlyph(self, name: str, **kwargs: Any) -> None:
r"""Remove the specified glyph from the default layer.
.. deprecated::
Use :meth:`BaseFont.__delitem__` instead.
This is the environment implementation of
:meth:`BaseFont.removeGlyph`.
:param name: The name of the glyph to remove. The value will be
normalized with :func:`normalizers.normalizeGlyphName`.
:param \**kwargs: Additional keyword arguments.
.. note::
Subclasses may override this method.
"""
layer = self.defaultLayer
layer.removeGlyph(name)
def __setitem__(self, name: str, glyph: BaseGlyph) -> BaseGlyph:
"""Insert the specified glyph into the font.
Example::
>>> glyph = font["A"] = otherGlyph
This will not insert a glyph directly, but rather create
a new :class:`BaseGlyph` instance containing the data from
`glyph`. The data inserted from `glyph` is the same data as
documented in :meth:`BaseGlyph.copy`.
On a font level, :attr:`.glyphOrder` will be preserved if
the `name` is already present.
:param name: The name to assign to the new glyph after
insertion.
:param glyph: The layer :class:`BaseGlyph` instance to insert.
:return: The newly inserted :class:`BaseGlyph` instance.
"""
name = normalizers.normalizeGlyphName(name)
if name in self:
# clear the glyph here if the glyph exists
dest = self._getItem(name)
dest.clear()
return self._insertGlyph(glyph, name=name, clear=False)
# order
glyphOrder: dynamicProperty = dynamicProperty(
"base_glyphOrder",
"""Get or set the order of the glyphs in the font.
The value must be a :class:`list` or :class:`tuple` of glyph names
reflecting the desired order of the font's :class:`BaseGlyph` objects.
:return: A :class:`tuple` of glyph names in their defined order.
Example::
>>> font.glyphOrder
["C", "B", "A"]
>>> font.glyphOrder = ("A", "B", "C")
""",
)
def _get_base_glyphOrder(self) -> tuple[str, ...]:
value = self._get_glyphOrder()
value = normalizers.normalizeGlyphOrder(value)
return value
def _set_base_glyphOrder(self, value: CollectionType[str]) -> None:
value = normalizers.normalizeGlyphOrder(value)
self._set_glyphOrder(value)
[docs]
def _get_glyphOrder(self) -> tuple[str, ...]: # type: ignore[return]
r"""Get the order of the glyphs in the native font.
This is the environment implementation of the
:attr:`BaseFont.glyphrOrder` property getter.
:param \**kwargs: Additional keyword arguments.
:return: A :class:`tuple` of layer names in their defined order.
The value will be normalized with :func:`normalizers.normalizeGlyphOrder`.
:raises NotImplementedError: If the method has not
beenoverridden by a subclass.
.. important::
Subclasses must override this method.
"""
self.raiseNotImplementedError()
[docs]
def _set_glyphOrder(self, value: CollectionType[str]) -> None:
r"""Set the order of the glyphs in the native font.
This is the environment implementation of the
:attr:`BaseFont.glyphrOrder` property setter.
:param value: A :class:`list` of glyph names reflecting the
desired order of the font's :class:`BaseGlyph` objects.
The value will have been normalized
with :func:`normalizers.normalizeGlyphOrder`.
: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()
# -----------------
# Global Operations
# -----------------
[docs]
def round(self) -> None:
"""Round all appropriate font data to integers.
This method applies only to the glyphs in the default layer
of the font. It is the equivalent of calling the :meth:`round`
method on:
- :attr:`info`
- :attr:`kerning`
- :attr:`defaultLayer`
- :attr:`guidelines`
Example::
>>> font.round()
"""
self._round()
[docs]
def _round(self) -> None:
"""Round all appropriate native font data to integers.
This is the environment implementation of :meth:`BaseFont.round`.
.. note::
Subclasses may override this method.
"""
layer = self.defaultLayer
layer.round()
self.info.round()
self.kerning.round()
for guideline in self.guidelines:
guideline.round()
[docs]
def autoUnicodes(self) -> None:
"""Use heuristics to set Unicode values in all font glyphs.
This method applies only to the glyphs in the default layer
of the font. Environments will define their own heuristics for
automatically determining values.
Example::
>>> font.autoUnicodes()
"""
self._autoUnicodes()
[docs]
def _autoUnicodes(self) -> None:
"""Use heuristics to set Unicode values in all native font glyphs.
This is the environment implementation of
:meth:`BaseFont.autoUnicodes`.
.. note::
Subclasses may override this method.
"""
layer = self.defaultLayer
layer.autoUnicodes()
# ----------
# Guidelines
# ----------
def _setFontInGuideline(self, guideline):
if guideline.font is None:
guideline.font = self
guidelines: dynamicProperty = dynamicProperty(
"guidelines",
"""Get the font-level guideline objects.
This property is read-only.
:return: A :class:`tuple` containing instances of
the :class:`BaseGuideline` class.
>>> for guideline in font.guidelines:
... guideline.angle
0
45
90
""",
)
[docs]
def _get_guidelines(self) -> tuple[BaseGuideline, ...]:
"""Get the native font-level guideline objects.
This is the environment implementation of
:attr:`BaseFont.guidelines`.
:return: A :class:`tuple` containing instances
of the :class:`BaseGuideline` subclass.
.. 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"""Return the number of font-level guidelines in the native font.
:param \**kwargs: Additional keyword arguments.
:return: An :class:`int` indicating the number of font-level
:class:`BaseGuideline` subclass instances in the font.
.. 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._setFontInGuideline(guideline)
return guideline
[docs]
def _getGuideline(self, index: int, **kwargs: Any) -> BaseGuideline: # type: ignore[return]
r"""Return the guideline at the given index.
:param index: The index of the guideline.
: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 new guideline to the font.
This method will create a new :class:`BaseGuideline` with the
provided values. Values may be copied from
the specified `guideline` or passed individually to the
appropriate parameters.
:param position: The optional position for the guideline as
a :ref:`type-coordinate`. Defaults to :obj:`None`.
:param angle: The optional angle for the guideline as
a :class:`float`. Defaults to :obj:`None`.
:param name: The optional name for the guideline as
a :class:`str`. Defaults to :obj:`None`.
:param color: The optional color for the guideline as
a :ref:`type-color`. Defaults to :obj:`None`.
:param guideline: The optional :class:`BaseGuideline` instance
from which to copy values. If `position`, `angle`, `name`,
or `color` are specified, those values will be used instead.
Defaults to :obj:`None`.
:return: The newly appended instance of
the :class:`BaseGuideline` class.
Example::
>>> guideline = font.appendGuideline((50, 0), 90)
>>> guideline = font.appendGuideline((0, 540), 0, name="overshoot",
... color=(0, 0, 0, 0.2))
"""
identifier = None
if guideline is not None:
normalizedGuideline = normalizers.normalizeGuideline(guideline)
if position is None:
position = normalizedGuideline.position
if angle is None:
angle = normalizedGuideline.angle
if name is None:
name = normalizedGuideline.name
if color is None:
color = normalizedGuideline.color
if normalizedGuideline.identifier is not None:
existing = {
g.identifier for g in self.guidelines if g.identifier is not None
}
if normalizedGuideline.identifier not in existing:
identifier = normalizedGuideline.identifier
if position is not None:
position = normalizers.normalizeCoordinateTuple(position)
else:
raise ValueError("Position cannot be None.")
if angle is not None:
angle = normalizers.normalizeRotationAngle(angle)
if name is not None:
name = normalizers.normalizeGuidelineName(name)
if color is not None:
color = normalizers.normalizeColor(color)
identifier = normalizers.normalizeIdentifier(identifier)
newGuideline = self._appendGuideline(
position, angle, name=name, color=color, identifier=identifier
)
newGuideline.font = self
return newGuideline
[docs]
def _appendGuideline( # type: ignore[return]
self,
position: CoordinateLike,
angle: float | None,
name: str | None,
color: RGBALike | None,
**kwargs: Any,
) -> BaseGuideline:
r"""Append a new guideline to the native font.
This is the environment implementation of
:meth:`BaseFont.appendGuideline`.
:param position: The position for the guideline as
a :ref:`type-coordinate`.
:param angle: The angle for the guideline as a :class:`float`.
:param name: The name for the guideline as a :class:`str`.
:param color: The color for the guideline as a :ref:`type-color`.
:param \**kwargs: Additional keyword arguments.
:return: The newly appended instance of
the :class:`BaseGuideline` subclass.
.. note::
Subclasses may override this method.
"""
self.raiseNotImplementedError()
[docs]
def removeGuideline(self, guideline: int | BaseGuideline) -> None:
"""Remove a guideline from the font.
:param guideline: A :class:`BaseGuideline` object or an integer
representing a :attr:`BaseGuideline.index`.
:raises: ValueError if no guideline is found at the given `index`.
Example::
>>> font.removeGuideline(guideline)
>>> font.removeGuideline(2)
"""
if isinstance(guideline, int):
index = guideline
else:
index = self._getGuidelineIndex(guideline)
normalizedIndex = normalizers.normalizeIndex(index)
# Avoid mypy conflict with normalizeIndex -> Optional[int]
if normalizedIndex is None:
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:
"""Remove the guideline at the specified index.
This is the environment implementation of
:meth:`BaseFont.removeGuideline`.
:param index: The index of the guideline to remove.
: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 font.
Example::
>>> font.clearGuidelines()
"""
self._clearGuidelines()
[docs]
def _clearGuidelines(self) -> None:
"""Clear all guidelines in the native font.
This is the environment implementation of
:meth:`BaseFont.clearGuidelines`.
.. note::
Subclasses may override this method.
"""
for _ in range(self._len__guidelines()):
self.removeGuideline(-1)
# -------------
# Interpolation
# -------------
[docs]
def interpolate(
self,
factor: InterpolationFactorLike,
minFont: BaseFont,
maxFont: BaseFont,
round: bool = True,
suppressError: bool = True,
) -> None:
"""Interpolate all possible data in the font.
The interpolation occurs on a 0 to 1.0 range between `minFont`
and `maxFont`, using the specified `factor`.
:param factor: The interpolation value as a single :class:`int`
or :class:`float` or a :class:`tuple` of two :class:`int`
or :class:`float` values representing the factors ``(x, y)``.
:param minFont: The :class:`BaseFont` instance corresponding to the 0.0
position in the interpolation.
:param maxFont: The :class:`BaseFont` 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 `minFont` or `maxFont` are not instances
of :class:`BaseFont`.
Example::
>>> font.interpolate(0.5, otherFont1, otherFont2)
>>> font.interpolate((0.5, 2.0), otherFont1, otherFont2, round=False)
"""
factor = normalizers.normalizeInterpolationFactor(factor)
if not isinstance(minFont, BaseFont):
raise TypeError(
f"Interpolation to an instance of {self.__class__.__name__!r} can not be performed from an instance of {minFont.__class__.__name__!r}."
)
if not isinstance(maxFont, BaseFont):
raise TypeError(
f"Interpolation to an instance of {self.__class__.__name__!r} can not be performed from an instance of {maxFont.__class__.__name__!r}."
)
round = normalizers.normalizeBoolean(round)
suppressError = normalizers.normalizeBoolean(suppressError)
self._interpolate(
factor, minFont, maxFont, round=round, suppressError=suppressError
)
[docs]
def _interpolate(
self,
factor: InterpolationFactorPair,
minFont: BaseFont,
maxFont: BaseFont,
round: bool,
suppressError: bool,
) -> None:
"""Interpolate all possible data in the native font.
This is the environment implementation of :meth:`BaseFont.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 minFont: The :class:`BaseFont` subclass instance
corresponding to the 0.0 position in the interpolation.
:param maxFont: The :class:`BaseFont` subclass instance
corresponding to the 1.0 position in the interpolation.
:param round: A boolean indicating whether the result should
be rounded to integers.
:param suppressError: A boolean indicating whether to ignore
incompatible data or raise an error when such incompatibilities
are found.
:raises TypeError: If `minFont` or `maxFont` are not instances
of :class:`BaseFont`.
.. note::
Subclasses may override this method.
"""
# layers
for layerName in self.layerOrder:
self.removeLayer(layerName)
for layerName in minFont.layerOrder:
if layerName not in maxFont.layerOrder:
continue
minLayer = minFont.getLayer(layerName)
maxLayer = maxFont.getLayer(layerName)
dstLayer = self.newLayer(layerName)
dstLayer.interpolate(
factor, minLayer, maxLayer, round=round, suppressError=suppressError
)
if self.layerOrder:
if ufoLib.DEFAULT_LAYER_NAME in self.layerOrder:
self.defaultLayer = self.getLayer(ufoLib.DEFAULT_LAYER_NAME)
else:
self.defaultLayer = self.getLayer(self.layerOrder[0])
# kerning and groups
self.kerning.interpolate(
factor,
minFont.kerning,
maxFont.kerning,
round=round,
suppressError=suppressError,
)
# info
self.info.interpolate(
factor, minFont.info, maxFont.info, round=round, suppressError=suppressError
)
compatibilityReporterClass = FontCompatibilityReporter
[docs]
def isCompatible(
self, other: BaseFont, cls=None
) -> tuple[bool, FontCompatibilityReporter]:
"""Evaluate interpolation compatibility with another font.
This method will return a :class:`bool` indicating if the font is
compatible for interpolation with `other`, and a :class:`str`
containing compatibility notes.
:param other: The other :class:`BaseFont` 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.FontCompatibilityReporter` instance.
Example::
>>> compatible, report = self.isCompatible(otherFont)
>>> compatible
False
>>> report
[Fatal] Glyph: "test1" + "test2"
[Fatal] Glyph: "test1" contains 1 contours | "test2" contains 2 contours
"""
return super().isCompatible(other, BaseFont)
[docs]
def _isCompatible(
self, other: BaseFont, reporter: FontCompatibilityReporter
) -> None:
"""Evaluate interpolation compatibility with another native font.
This is the environment implementation of :meth:`BaseFont.isCompatible`.
:param other: The other :class:`BaseFont` subclass instance to check
compatibility with.
:param reporter: An object used to report compatibility issues.
.. note::
Subclasses may override this method.
"""
font1 = self
font2 = other
# incompatible guidelines
guidelines1 = set(font1.guidelines)
guidelines2 = set(font2.guidelines)
if len(guidelines1) != len(guidelines2):
reporter.warning = True
reporter.guidelineCountDifference = True
if len(guidelines1.difference(guidelines2)) != 0:
reporter.warning = True
reporter.guidelinesMissingFromFont2 = list(
guidelines1.difference(guidelines2)
)
if len(guidelines2.difference(guidelines1)) != 0:
reporter.warning = True
reporter.guidelinesMissingInFont1 = list(
guidelines2.difference(guidelines1)
)
# incompatible layers
layers1 = set(font1.layerOrder)
layers2 = set(font2.layerOrder)
if len(layers1) != len(layers2):
reporter.warning = True
reporter.layerCountDifference = True
if len(layers1.difference(layers2)) != 0:
reporter.warning = True
reporter.layersMissingFromFont2 = list(layers1.difference(layers2))
if len(layers2.difference(layers1)) != 0:
reporter.warning = True
reporter.layersMissingInFont1 = list(layers2.difference(layers1))
# test layers
for layerName in sorted(layers1.intersection(layers2)):
layer1 = font1.getLayer(layerName)
layer2 = font2.getLayer(layerName)
layerCompatibility = layer1.isCompatible(layer2)[1]
if layerCompatibility.fatal or layerCompatibility.warning:
if layerCompatibility.fatal:
reporter.fatal = True
if layerCompatibility.warning:
reporter.warning = True
reporter.layers.append(layerCompatibility)
# -------
# mapping
# -------
[docs]
def getReverseComponentMapping(self) -> ReverseComponentMappingType:
"""Get a reversed map of all component references in the font.
This method creates a :class:`dict` mapping each component glyph
name in the font to a :class:`tuple` containing the composite
glyph names that include the comoponent.
:return: A :class:`dict` of component glyph names mapped to
tuples of composite glyph names.
Example::
>>> mapping = font.getReverseComponentMapping()
>>> mapping
{'A': ('Aacute', 'Aring'), 'acute': ('Aacute',),
'ring': ('Aring',) , ...}
"""
return self._getReverseComponentMapping()
[docs]
def _getReverseComponentMapping(self) -> ReverseComponentMappingType:
"""Get a reversed map of all component references in the font.
This is the environment implementation of
:meth:`BaseFont.getReverseComponentMapping`.
.. note::
Subclasses may override this method.
"""
layer = self.defaultLayer
return layer.getReverseComponentMapping()
[docs]
def getCharacterMapping(self) -> CharacterMappingType:
"""Get the font's character mapping.
This method creates a :class:`dict` mapping Unicode values to
tuples of glyph names. Each Unicode value corresponds to one or
more glyphs, and the glyph names represent these glyphs in the
mapping.
.. note::
One glyph can have multiple unicode values, and a unicode value can
have multiple glyphs pointing to it.
:return: A :class:`dict` mapping Unicode values to :class:`tuple` of
glyph names.
Example::
>>> mapping = font.getCharacterMapping()
>>> mapping
{65: ('A', 'A.alt'), 66: ('B',),
67: ('C', 'C.alt', 'C.swash') , ...}
"""
return self._getCharacterMapping()
[docs]
def _getCharacterMapping(self) -> CharacterMappingType:
"""Get the native font's character mapping.
This is the environment implementation of
:meth:`BaseFont.getCharacterMapping`.
:return: A :class:`dict` mapping Unicode values to :class:`tuple` of
glyph names.
:raises NotImplementedError: If the method has not been
overridden by a subclass.
.. note::
Subclasses may override this method.
"""
layer = self.defaultLayer
return layer.getCharacterMapping()
# ---------
# Selection
# ---------
# layers
selectedLayers: dynamicProperty = dynamicProperty(
"base_selectedLayers",
"""Get or set the selected glyph layers in the default font layer.
:param value: The :class:`list` of :class:`BaseLayer` instances
to select.
:return: A :class:`tuple` of currently selected :class:`BaseLayer`
instances.
Getting selected layer objects::
>>> for layer in layer.selectedLayers:
... layer.color = (1, 0, 0, 0.5)
Setting selected layer objects::
>>> layer.selectedLayers = someLayers
""",
)
def _get_base_selectedLayers(self) -> tuple[BaseLayer, ...]:
selected = tuple(
normalizers.normalizeLayer(layer) for layer in self._get_selectedLayers()
)
return selected
[docs]
def _get_selectedLayers(self) -> tuple[BaseLayer, ...]:
"""Get the selected glyph layers in the native default font layer.
This is the environment implementation of
the :attr:`BaseFont.selectedLayers` property getter.
:return: A :class:`tuple` of currently selected :class:`BaseLayer`
instances. Each value item will be normalized
with :func:`normalizers.normalizeLayer`.
.. note::
Subclasses may override this method.
"""
return self._getSelectedSubObjects(self.layers)
def _set_base_selectedLayers(self, value: list[BaseLayer]) -> None:
normalized = [normalizers.normalizeLayer(layer) for layer in value]
self._set_selectedLayers(normalized)
[docs]
def _set_selectedLayers(self, value: list[BaseLayer]) -> None:
"""Set the selected glyph layers in the native default font layer.
This is the environment implementation of
the :attr:`BaseFont.selectedLayers` property setter.
:param value: The :class:`list` of :class:`BaseLayer` instances
to select. Each value item will have been normalized
with :func:`normalizers.normalizeLayer`.
.. note::
Subclasses may override this method.
"""
return self._setSelectedSubObjects(self.layers, value)
selectedLayerNames: dynamicProperty = dynamicProperty(
"base_selectedLayerNames",
"""Get or set the selected glyph layer names in the default font layer.
:param value: The :class:`list` of layer names representing
the :class:`BaseLayer` instances to select.
:return: A :class:`tuple` of layer names representing the currently
selected :class:`BaseLayer` instances.
Getting selected layer names::
>>> for name in layer.selectedLayerNames:
... print(name)
Setting selected layer names:
>>> layer.selectedLayerNames = ["A", "B", "C"]
""",
)
def _get_base_selectedLayerNames(self) -> tuple[str, ...]:
selected = tuple(
normalizers.normalizeLayerName(name)
for name in self._get_selectedLayerNames()
)
return selected
[docs]
def _get_selectedLayerNames(self) -> tuple[str, ...]:
"""Get the selected glyph layer names in the native font layer.
This is the environment implementation of
the :attr:`BaseFont.selectedLayerNames` property getter.
:return: A :class:`tuple` of layer names representing the currently
selected :class:`BaseLayer` instances. Each value item will be
normalized with :func:`normalizers.normalizeLayerName`.
.. note::
Subclasses may override this method.
"""
return tuple(layer.name for layer in self.selectedLayers)
def _set_base_selectedLayerNames(self, value: list[str]) -> None:
normalized = [normalizers.normalizeLayerName(name) for name in value]
self._set_selectedLayerNames(normalized)
[docs]
def _set_selectedLayerNames(self, value: list[str]) -> None:
"""Set the selected glyph layer names in the native font layer.
This is the environment implementation of
the :attr:`BaseFont.selectedLayerNames` property setter.
:param value: The :class:`list` of layer names representing
the :class:`BaseLayer` instances to select. Each value item will
have been normalized with :func:`normalizers.normalizeLayerName`.
.. note::
Subclasses may override this method.
"""
select = [self.layers(name) for name in value]
self.selectedLayers = select
# guidelines
selectedGuidelines: dynamicProperty = dynamicProperty(
"base_selectedGuidelines",
"""Get or set the selected guidelines in the font.
:param value: The :class:`list` of either :class:`BaseGuideline` instances
to select or their corresponding indexes.
:return: A :class:`tuple` of currently selected :class:`BaseGuideline`
instances.
Getting selected guideline objects::
>>> for guideline in font.selectedGuidelines:
... guideline.color = (1, 0, 0, 0.5)
Setting selected guideline objects::
>>> font.selectedGuidelines = someGuidelines
Setting also supports guideline indexes::
>>> font.selectedGuidelines = [0, 2]
""",
)
def _get_base_selectedGuidelines(self) -> tuple[BaseGuideline, ...]:
selected = tuple(
normalizers.normalizeGuideline(guideline)
for guideline in self._get_selectedGuidelines()
)
return selected
[docs]
def _get_selectedGuidelines(self) -> tuple[BaseGuideline, ...]:
"""Get the selected guidelines in the native font.
This is the environment implementation of
the :attr:`BaseFont.selectedGuidelines` property getter.
:return: A :class:`tuple` of 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: list[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:
continue
normalizedGuideline = normalizedIndex
else:
normalizedGuideline = normalizers.normalizeGuideline(guideline)
normalized.append(normalizedGuideline)
self._set_selectedGuidelines(normalized)
[docs]
def _set_selectedGuidelines(self, value: list[BaseGuideline | int]) -> None:
"""Set the selected guidelines in the native font.
This is the environment implementation of
the :attr:`BaseFont.selectedGuidelines` property setter.
:param value: The :class:`list` of :class:`BaseGuideline` instances to
select. Each value item will have been normalized
with :func:`normalizers.normalizeGuideline`.
.. note::
Subclasses may override this method.
"""
return self._setSelectedSubObjects(self.guidelines, value)