from typing import (
Any,
Dict,
Iterator,
KeysView,
Optional,
Sequence,
Set,
Union,
overload,
)
from attr import define, field
from fontTools.ufoLib.glifLib import GlyphSet
from ufoLib2.constants import DEFAULT_LAYER_NAME
from ufoLib2.objects.glyph import Glyph
from ufoLib2.objects.misc import (
_NOT_LOADED,
BoundingBox,
Placeholder,
_deepcopy_unlazify_attrs,
_prune_object_libs,
unionBounds,
)
from ufoLib2.typing import T
def _convert_glyphs(
value: Union[Dict[str, Union[Glyph, Placeholder]], Sequence[Glyph]]
) -> Dict[str, Union[Glyph, Placeholder]]:
result: Dict[str, Union[Glyph, Placeholder]] = {}
glyph_ids = set()
if isinstance(value, dict):
for name, glyph in value.items():
if not isinstance(glyph, Placeholder):
if not isinstance(glyph, Glyph):
raise TypeError(f"Expected Glyph, found {type(glyph).__name__}")
glyph_id = id(glyph)
if glyph_id in glyph_ids:
raise KeyError(f"{glyph!r} can't be added twice")
glyph_ids.add(glyph_id)
if glyph.name is None:
glyph._name = name
elif glyph.name != name:
raise ValueError(
"glyph has incorrect name: "
f"expected '{name}', found '{glyph.name}'"
)
result[name] = glyph
else:
for glyph in value:
if not isinstance(glyph, Glyph):
raise TypeError(f"Expected Glyph, found {type(glyph).__name__}")
glyph_id = id(glyph)
if glyph_id in glyph_ids:
raise KeyError(f"{glyph!r} can't be added twice")
glyph_ids.add(glyph_id)
if glyph.name is None:
raise ValueError(f"{glyph!r} has no name; can't add it to Layer")
if glyph.name in result:
raise KeyError(f"glyph named '{glyph.name}' already exists")
result[glyph.name] = glyph
return result
[docs]@define
class Layer:
"""Represents a Layer that holds Glyph objects.
See http://unifiedfontobject.org/versions/ufo3/glyphs/layerinfo.plist/.
Note:
Various methods that work on Glyph objects take a ``layer`` attribute, because
the UFO data model prescribes that Components within a Glyph object refer to
glyphs *within the same layer*.
Behavior:
Layer behaves **partly** like a dictionary of type ``Dict[str, Glyph]``.
Unless the font is loaded eagerly (with ``lazy=False``), the Glyph objects
by default are only loaded into memory when accessed.
To get the number of glyphs in the layer::
glyphCount = len(layer)
To iterate over all glyphs::
for glyph in layer:
...
To check if a specific glyph exists::
exists = "myGlyphName" in layer
To get a specific glyph::
layer["myGlyphName"]
To delete a specific glyph::
del layer["myGlyphName"]
"""
_name: str = DEFAULT_LAYER_NAME
_glyphs: Dict[str, Union[Glyph, Placeholder]] = field(
factory=dict, converter=_convert_glyphs
)
color: Optional[str] = None
"""The color assigned to the layer."""
lib: Dict[str, Any] = field(factory=dict)
"""The layer's lib for mapping string keys to arbitrary data."""
_glyphSet: Any = field(default=None, init=False, eq=False)
[docs] @classmethod
def read(cls, name: str, glyphSet: GlyphSet, lazy: bool = True) -> "Layer":
"""Instantiates a Layer object from a
:class:`fontTools.ufoLib.glifLib.GlyphSet`.
Args:
name: The name of the layer.
glyphSet: The GlyphSet object to read from.
lazy: If True, load glyphs as they are accessed. If False, load everything
up front.
"""
glyphNames = glyphSet.keys()
glyphs: Dict[str, Union[Glyph, Placeholder]]
if lazy:
glyphs = {gn: _NOT_LOADED for gn in glyphNames}
else:
glyphs = {}
for glyphName in glyphNames:
glyph = Glyph(glyphName)
glyphSet.readGlyph(glyphName, glyph, glyph.getPointPen())
glyphs[glyphName] = glyph
self = cls(name, glyphs)
if lazy:
self._glyphSet = glyphSet
glyphSet.readLayerInfo(self)
return self
[docs] def unlazify(self) -> None:
"""Load all glyphs into memory."""
for _ in self:
pass
__deepcopy__ = _deepcopy_unlazify_attrs
def __contains__(self, name: object) -> bool:
return name in self._glyphs
def __delitem__(self, name: str) -> None:
del self._glyphs[name]
def __getitem__(self, name: str) -> Glyph:
glyph_object = self._glyphs[name]
if isinstance(glyph_object, Placeholder):
return self.loadGlyph(name)
return glyph_object
def __setitem__(self, name: str, glyph: Glyph) -> None:
if not isinstance(glyph, Glyph):
raise TypeError(f"Expected Glyph, found {type(glyph).__name__}")
glyph._name = name
self._glyphs[name] = glyph
def __iter__(self) -> Iterator[Glyph]:
for name in self._glyphs:
yield self[name]
def __len__(self) -> int:
return len(self._glyphs)
def __repr__(self) -> str:
n = len(self._glyphs)
return "<{}.{} '{}' ({}) at {}>".format(
self.__class__.__module__,
self.__class__.__name__,
self._name,
"empty" if n == 0 else "{} glyph{}".format(n, "s" if n > 1 else ""),
hex(id(self)),
)
[docs] def get(self, name: str, default: Optional[T] = None) -> Union[Optional[T], Glyph]:
"""Return the Glyph object for name if it is present in this layer,
otherwise return ``default``."""
try:
return self[name]
except KeyError:
return default
[docs] def keys(self) -> KeysView[str]:
"""Returns a list of glyph names."""
return self._glyphs.keys()
@overload
def pop(self, key: str) -> Glyph:
...
@overload
def pop(self, key: str, default: Union[Glyph, T] = ...) -> Union[Glyph, T]:
...
[docs] def pop(self, key: str, default: Union[Glyph, T] = KeyError) -> Union[Glyph, T]: # type: ignore
"""Remove and return glyph from layer.
Args:
key: The name of the glyph.
default: What to return if there is no glyph with the given name.
"""
# NOTE: We can't defer to self._glyphs.pop because we must load glyphs
try:
glyph = self[key]
except KeyError:
if default is KeyError:
raise
glyph = default # type: ignore
else:
del self[key]
return glyph
@property
def name(self) -> str:
"""The name of the layer."""
return self._name
@property
def bounds(self) -> Optional[BoundingBox]:
"""Returns the (xMin, yMin, xMax, yMax) bounding box of the layer,
taking the actual contours into account.
|defcon_compat|
"""
bounds = None
for glyph in self:
bounds = unionBounds(bounds, glyph.getBounds(self))
return bounds
@property
def controlPointBounds(self) -> Optional[BoundingBox]:
"""Returns the (xMin, yMin, xMax, yMax) bounding box of the layer,
taking only the control points into account.
|defcon_compat|
"""
bounds = None
for glyph in self:
bounds = unionBounds(bounds, glyph.getControlBounds(self))
return bounds
[docs] def addGlyph(self, glyph: Glyph) -> None:
"""Appends glyph object to the this layer unless its name is already
taken."""
self.insertGlyph(glyph, overwrite=False, copy=False)
[docs] def insertGlyph(
self,
glyph: Glyph,
name: Optional[str] = None,
overwrite: bool = True,
copy: bool = True,
) -> None:
"""Inserts Glyph object into this layer.
Args:
glyph: The Glyph object.
name: The name of the glyph.
overwrite: If True, overwrites (read: deletes) glyph with the same name if
it exists. If False, raises KeyError.
copy: If True, copies the Glyph object before insertion. If False, inserts
as is.
"""
if copy:
glyph = glyph.copy()
if name is not None:
glyph._name = name
if glyph.name is None:
raise ValueError(f"{glyph!r} has no name; can't add it to Layer")
if not overwrite and glyph.name in self._glyphs:
raise KeyError(f"glyph named '{glyph.name}' already exists")
self._glyphs[glyph.name] = glyph
[docs] def loadGlyph(self, name: str) -> Glyph:
"""Load and return Glyph object."""
# XXX: Remove and let __getitem__ do it?
glyph = Glyph(name)
self._glyphSet.readGlyph(name, glyph, glyph.getPointPen())
self._glyphs[name] = glyph
return glyph
[docs] def newGlyph(self, name: str) -> Glyph:
"""Creates and returns new Glyph object in this layer with name."""
if name in self._glyphs:
raise KeyError(f"glyph named '{name}' already exists")
self._glyphs[name] = glyph = Glyph(name)
return glyph
[docs] def renameGlyph(self, name: str, newName: str, overwrite: bool = False) -> None:
"""Renames a Glyph object in this layer.
Args:
name: The old name.
newName: The new name.
overwrite: If False, raises exception if newName is already taken.
If True, overwrites (read: deletes) the old Glyph object.
"""
if name == newName:
return
if not overwrite and newName in self._glyphs:
raise KeyError(f"target glyph named '{newName}' already exists")
# pop and set name
glyph = self.pop(name)
glyph._name = newName
# add it back
self._glyphs[newName] = glyph
[docs] def instantiateGlyphObject(self) -> Glyph:
"""Returns a new Glyph instance.
|defcon_compat|
"""
return Glyph()
[docs] def write(self, glyphSet: GlyphSet, saveAs: bool = True) -> None:
"""Write Layer to a :class:`fontTools.ufoLib.glifLib.GlyphSet`.
Args:
glyphSet: The GlyphSet object to write to.
saveAs: If True, tells the writer to save out-of-place. If False, tells the
writer to save in-place. This affects how resources are cleaned before
writing.
"""
glyphs = self._glyphs
if not saveAs:
for name in set(glyphSet.contents).difference(glyphs):
glyphSet.deleteGlyph(name)
for name, glyph in glyphs.items():
if isinstance(glyph, Placeholder):
if saveAs:
glyph = self.loadGlyph(name)
else:
continue
_prune_object_libs(glyph.lib, _fetch_glyph_identifiers(glyph))
glyphSet.writeGlyph(
name, glyphObject=glyph, drawPointsFunc=glyph.drawPoints
)
glyphSet.writeContents()
glyphSet.writeLayerInfo(self)
if saveAs:
# all glyphs are loaded by now, no need to keep ref to glyphSet
self._glyphSet = None
def _fetch_glyph_identifiers(glyph: Glyph) -> Set[str]:
"""Returns all identifiers in use in a glyph."""
identifiers = set()
for anchor in glyph.anchors:
if anchor.identifier is not None:
identifiers.add(anchor.identifier)
for guideline in glyph.guidelines:
if guideline.identifier is not None:
identifiers.add(guideline.identifier)
for contour in glyph.contours:
if contour.identifier is not None:
identifiers.add(contour.identifier)
for point in contour:
if point.identifier is not None:
identifiers.add(point.identifier)
for component in glyph.components:
if component.identifier is not None:
identifiers.add(component.identifier)
return identifiers