Source code for ufoLib2.objects.layerSet

from collections import OrderedDict
from typing import AbstractSet, Any, Iterable, Iterator, List, Optional, Sized, Union

from attr import define, field
from fontTools.ufoLib import UFOReader, UFOWriter

from ufoLib2.constants import DEFAULT_LAYER_NAME
from ufoLib2.errors import Error
from ufoLib2.objects.layer import Layer
from ufoLib2.objects.misc import _NOT_LOADED, Placeholder, _deepcopy_unlazify_attrs
from ufoLib2.typing import T


def _must_have_at_least_one_item(self: Any, attribute: Any, value: Sized) -> None:
    if not len(value):
        raise ValueError("value must have at least one item.")


[docs]@define class LayerSet: """Represents a mapping of layer names to Layer objects. See http://unifiedfontobject.org/versions/ufo3/layercontents.plist/ for layer semantics. Behavior: LayerSet behaves **partly** like a dictionary of type ``Dict[str, Layer]``, but creating and loading layers is done through their own methods. Unless the font is loaded eagerly (with ``lazy=False``), the layer objects and their glyphs are by default only loaded into memory when accessed. To get the number of layers in the font:: layerCount = len(font.layers) To iterate over all layers:: for layer in font.layers: ... To check if a specific layer exists:: exists = "myLayerName" in font.layers To get a specific layer:: font.layers["myLayerName"] To delete a specific layer:: del font.layers["myLayerName"] """ _layers: "OrderedDict[str, Union[Layer, Placeholder]]" = field( validator=_must_have_at_least_one_item, ) defaultLayer: Layer """The Layer that is marked as the default, typically named ``public.default``.""" _reader: Optional[UFOReader] = field(default=None, init=False, eq=False) def __attrs_post_init__(self) -> None: if not any(layer is self.defaultLayer for layer in self._layers.values()): raise ValueError( f"Default layer {repr(self.defaultLayer)} must be in layer set." )
[docs] @classmethod def default(cls) -> "LayerSet": """Return a new LayerSet with an empty default Layer.""" return cls.from_iterable([Layer()])
[docs] @classmethod def from_iterable( cls, value: Iterable[Layer], defaultLayerName: str = DEFAULT_LAYER_NAME ) -> "LayerSet": """Instantiates a LayerSet from an iterable of :class:`.Layer` objects. Args: value: an iterable of :class:`.Layer` objects. defaultLayerName: the name of the default layer of the ones in ``value``. """ layers: OrderedDict[str, Union[Layer, Placeholder]] = OrderedDict() defaultLayer = None for layer in value: if not isinstance(layer, Layer): raise TypeError(f"expected 'Layer', found '{type(layer).__name__}'") if layer.name in layers: raise KeyError(f"duplicate layer name: '{layer.name}'") if layer.name == defaultLayerName: defaultLayer = layer layers[layer.name] = layer if defaultLayerName not in layers: raise ValueError(f"expected one layer named '{defaultLayerName}'.") assert defaultLayer is not None return cls(layers=layers, defaultLayer=defaultLayer)
[docs] @classmethod def read(cls, reader: UFOReader, lazy: bool = True) -> "LayerSet": """Instantiates a LayerSet object from a :class:`fontTools.ufoLib.UFOReader`. Args: path: The path to the UFO to load. lazy: If True, load glyphs, data files and images as they are accessed. If False, load everything up front. """ layers: OrderedDict[str, Union[Layer, Placeholder]] = OrderedDict() defaultLayer = None defaultLayerName = reader.getDefaultLayerName() for layerName in reader.getLayerNames(): isDefault = layerName == defaultLayerName if isDefault or not lazy: layer = cls._loadLayer(reader, layerName, lazy) if isDefault: defaultLayer = layer layers[layerName] = layer else: layers[layerName] = _NOT_LOADED assert defaultLayer is not None self = cls(layers=layers, defaultLayer=defaultLayer) if lazy: self._reader = reader return self
[docs] def unlazify(self) -> None: """Load all layers into memory.""" for layer in self: layer.unlazify()
__deepcopy__ = _deepcopy_unlazify_attrs @staticmethod def _loadLayer(reader: UFOReader, layerName: str, lazy: bool = True) -> Layer: glyphSet = reader.getGlyphSet(layerName) return Layer.read(layerName, glyphSet, lazy=lazy) def loadLayer(self, layerName: str, lazy: bool = True) -> Layer: # XXX: Remove this method and do business via _loadLayer or take this one # private. assert self._reader is not None if layerName not in self._layers: raise KeyError(layerName) layer = self._loadLayer(self._reader, layerName, lazy) self._layers[layerName] = layer return layer def __contains__(self, name: str) -> bool: return name in self._layers def __delitem__(self, name: str) -> None: if self.defaultLayer is not None: if name == self.defaultLayer.name: raise KeyError("cannot delete default layer %r" % name) del self._layers[name] def __getitem__(self, name: str) -> Layer: layer_object = self._layers[name] if isinstance(layer_object, Placeholder): return self.loadLayer(name) return layer_object def __iter__(self) -> Iterator[Layer]: for layer_name, layer_object in self._layers.items(): if isinstance(layer_object, Placeholder): yield self.loadLayer(layer_name) else: yield layer_object def __len__(self) -> int: return len(self._layers)
[docs] def get(self, name: str, default: Optional[T] = None) -> Union[Optional[T], Layer]: try: return self[name] except KeyError: return default
[docs] def keys(self) -> AbstractSet[str]: return self._layers.keys()
def __repr__(self) -> str: n = len(self._layers) return "<{}.{} ({} layer{}) at {}>".format( self.__class__.__module__, self.__class__.__name__, n, "s" if n > 1 else "", hex(id(self)), ) @property def layerOrder(self) -> List[str]: """The font's layer order. Getter: Returns the font's layer order. Note: The getter always returns a new list, modifications to it do not change the LayerSet. Setter: Sets the font's layer order. The set order value must contain all layers that are present in the LayerSet. """ return list(self._layers) @layerOrder.setter def layerOrder(self, order: List[str]) -> None: if set(order) != set(self._layers): raise Error( "`order` must contain the same layers that are currently present." ) layers = OrderedDict() for name in order: layers[name] = self._layers[name] self._layers = layers
[docs] def newLayer(self, name: str, **kwargs: Any) -> Layer: """Creates and returns a named layer. Args: name: The layer name. kwargs: Arguments passed to the constructor of Layer. """ if name in self._layers: raise KeyError("layer %r already exists" % name) self._layers[name] = layer = Layer(name, **kwargs) return layer
[docs] def renameGlyph(self, name: str, newName: str, overwrite: bool = False) -> None: """Renames a glyph across all layers. Args: name: The old name. newName: The new name. overwrite: If False, raises exception if newName is already taken in any layer. If True, overwrites (read: deletes) the old Glyph object. """ # Note: this would be easier if the glyph contained the layers! if name == newName: return # make sure we're copying something if not any(name in layer for layer in self): raise KeyError("name %r is not in layer set" % name) # prepare destination, delete if overwrite=True or error for layer in self: if newName in layer: if overwrite: del layer[newName] else: raise KeyError("target name %r already exists" % newName) # now do the move for layer in self: if name in layer: layer[newName] = glyph = layer.pop(name) glyph._name = newName
[docs] def renameLayer(self, name: str, newName: str, overwrite: bool = False) -> None: """Renames a 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 Layer object. """ if name == newName: return if not overwrite and newName in self._layers: raise KeyError("target name %r already exists" % newName) layer = self[name] del self._layers[name] self._layers[newName] = layer layer._name = newName
[docs] def write(self, writer: UFOWriter, saveAs: Optional[bool] = None) -> None: """Writes this LayerSet to a :class:`fontTools.ufoLib.UFOWriter`. Args: writer(fontTools.ufoLib.UFOWriter): The writer 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. """ if saveAs is None: saveAs = self._reader is not writer # if in-place, remove deleted layers layers = self._layers if not saveAs: for name in set(writer.getLayerNames()).difference(layers): writer.deleteGlyphSet(name) # write layers defaultLayer = self.defaultLayer for name, layer in layers.items(): default = layer is defaultLayer if isinstance(layer, Placeholder): if saveAs: layer = self.loadLayer(name, lazy=False) else: continue glyphSet = writer.getGlyphSet(name, defaultLayer=default) layer.write(glyphSet, saveAs=saveAs) writer.writeLayerContents(self.layerOrder)