diff --git a/src/ufoLib2/objects/layer.py b/src/ufoLib2/objects/layer.py index f1cb1585..9cc84205 100644 --- a/src/ufoLib2/objects/layer.py +++ b/src/ufoLib2/objects/layer.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import ( Any, Dict, @@ -6,15 +7,22 @@ Optional, Sequence, Set, + Tuple, Union, overload, ) import attr +import readwrite_ufo_glif from fontTools.ufoLib.glifLib import GlyphSet from ufoLib2.constants import DEFAULT_LAYER_NAME +from ufoLib2.objects.anchor import Anchor +from ufoLib2.objects.component import Component +from ufoLib2.objects.contour import Contour from ufoLib2.objects.glyph import Glyph +from ufoLib2.objects.guideline import Guideline +from ufoLib2.objects.image import Image from ufoLib2.objects.misc import ( _NOT_LOADED, BoundingBox, @@ -23,6 +31,7 @@ _prune_object_libs, unionBounds, ) +from ufoLib2.objects.point import Point from ufoLib2.typing import T @@ -64,6 +73,16 @@ def _convert_glyphs( return result +@attr.s(auto_attribs=True, slots=True, repr=False) +class MinimalGlyphSet: + path: Path + contents: Dict[str, str] + + @classmethod + def from_path(cls, path: Path) -> "MinimalGlyphSet": + return cls(path, readwrite_ufo_glif.read_layer_contents(str(path))) + + @attr.s(auto_attribs=True, slots=True, repr=False) class Layer: """Represents a Layer that holds Glyph objects. @@ -115,7 +134,7 @@ class Layer: _glyphSet: Any = attr.ib(default=None, init=False, eq=False) @classmethod - def read(cls, name: str, glyphSet: GlyphSet, lazy: bool = True) -> "Layer": + def read(cls, name: str, path: Path, lazy: bool = True) -> "Layer": """Instantiates a Layer object from a :class:`fontTools.ufoLib.glifLib.GlyphSet`. @@ -125,20 +144,20 @@ def read(cls, name: str, glyphSet: GlyphSet, lazy: bool = True) -> "Layer": 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} + glyphset = MinimalGlyphSet.from_path(path) + glyphs = {gn: _NOT_LOADED for gn in glyphset.contents.keys()} + color, lib = readwrite_ufo_glif.read_layerinfo_maybe(str(path)) + self = cls(name, glyphs, color=color, lib=lib) + self._glyphSet = glyphset 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) + glyphs, layerinfo = _read_layer(path) + self = cls( + name, glyphs, color=layerinfo.get("color"), lib=layerinfo.get("lib") + ) + return self def unlazify(self) -> None: @@ -285,9 +304,9 @@ def insertGlyph( 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 + layer_path: Path = self._glyphSet.path + glif_path = layer_path / self._glyphSet.contents[name] + self._glyphs[name] = glyph = _read_glyph(glif_path, name) return glyph def newGlyph(self, name: str) -> Glyph: @@ -323,6 +342,7 @@ def instantiateGlyphObject(self) -> Glyph: """ return Glyph() + # where does writer glyphset come from? same as layer._glyphSet? def write(self, glyphSet: GlyphSet, saveAs: bool = True) -> None: """Write Layer to a :class:`fontTools.ufoLib.glifLib.GlyphSet`. @@ -373,3 +393,59 @@ def _fetch_glyph_identifiers(glyph: Glyph) -> Set[str]: if component.identifier is not None: identifiers.add(component.identifier) return identifiers + + +def _read_glyph(glif_path: str, name: str) -> Glyph: + # data = pickle.loads(readwrite_ufo_glif.read_glyph(glif_path)) + data = readwrite_ufo_glif.read_glyph(str(glif_path)) + return Glyph( + name, + height=data.get("height", 0), + width=data.get("width", 0), + unicodes=data.get("unicodes", []), + image=Image(**data["image"]) if "image" in data else Image(), + anchors=[Anchor(**kwargs) for kwargs in data.get("anchors", [])], + guidelines=[Guideline(**kwargs) for kwargs in data.get("guidelines", [])], + lib=data.get("lib", {}), + note=data.get("note", {}), + contours=[ + Contour( + points=[Point(**kwargs) for kwargs in contour["points"]], + identifier=contour.get("identifier"), + ) + for contour in data.get("contours", []) + ], + components=[Component(**kwargs) for kwargs in data.get("components", [])], + ) + + +def _read_layer(layer_path: str) -> Tuple[Dict[str, Glyph], Dict[str, Any]]: + # all_data: Dict[str, Dict[str, Any]] = pickle.loads(readwrite_ufo_glif.read_layer(layer_path)) + all_data: Dict[str, Dict[str, Any]] + layerinfo: Dict[str, Any] + all_data, layerinfo = readwrite_ufo_glif.read_layer(str(layer_path)) + + glyphs = { + name: Glyph( + name, + height=data.get("height", 0), + width=data.get("width", 0), + unicodes=data.get("unicodes", []), + image=Image(**data["image"]) if "image" in data else Image(), + anchors=[Anchor(**kwargs) for kwargs in data.get("anchors", [])], + guidelines=[Guideline(**kwargs) for kwargs in data.get("guidelines", [])], + lib=data.get("lib", {}), + note=data.get("note", {}), + contours=[ + Contour( + points=[Point(**kwargs) for kwargs in contour["points"]], + identifier=contour.get("identifier"), + ) + for contour in data.get("contours", []) + ], + components=[Component(**kwargs) for kwargs in data.get("components", [])], + ) + for name, data in all_data.items() + } + + return glyphs, layerinfo diff --git a/src/ufoLib2/objects/layerSet.py b/src/ufoLib2/objects/layerSet.py index e8a2d744..388fcb35 100644 --- a/src/ufoLib2/objects/layerSet.py +++ b/src/ufoLib2/objects/layerSet.py @@ -1,5 +1,16 @@ from collections import OrderedDict -from typing import AbstractSet, Any, Iterable, Iterator, List, Optional, Sized, Union +from pathlib import Path +from typing import ( + AbstractSet, + Any, + Iterable, + Iterator, + List, + Optional, + Sized, + Tuple, + Union, +) import attr from fontTools.ufoLib import UFOReader, UFOWriter @@ -112,10 +123,13 @@ def read(cls, reader: UFOReader, lazy: bool = True) -> "LayerSet": defaultLayerName = reader.getDefaultLayerName() + base_path = Path(reader.fs.getsyspath(".")) + layer_contents = reader._readLayerContents(validate=True) for layerName in reader.getLayerNames(): isDefault = layerName == defaultLayerName if isDefault or not lazy: - layer = cls._loadLayer(reader, layerName, lazy) + layer_path = base_path / cls._layer_dir(layerName, layer_contents) + layer = cls._loadLayer(layer_path, layerName, lazy) if isDefault: defaultLayer = layer layers[layerName] = layer @@ -138,9 +152,9 @@ def unlazify(self) -> None: __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(layer_path: Path, layerName: str, lazy: bool = True) -> Layer: + # glyphSet = reader.getGlyphSet(layerName) + return Layer.read(layerName, layer_path, lazy=lazy) def loadLayer(self, layerName: str, lazy: bool = True) -> Layer: # XXX: Remove this method and do business via _loadLayer or take this one @@ -148,10 +162,24 @@ def loadLayer(self, layerName: str, lazy: bool = True) -> Layer: assert self._reader is not None if layerName not in self._layers: raise KeyError(layerName) - layer = self._loadLayer(self._reader, layerName, lazy) + base_path = Path(self._reader.fs.getsyspath(".")) + layer_contents = self._reader._readLayerContents(validate=True) + layer_path = base_path / self._layer_dir(layerName, layer_contents) + layer = self._loadLayer(layer_path, layerName, lazy) self._layers[layerName] = layer return layer + @staticmethod + def _layer_dir(layer_name: str, layer_contents: List[Tuple[str, str]]) -> str: + directory = None + for stored_layer_name, stored_layer_directory in layer_contents: + if layer_name == stored_layer_name: + directory = stored_layer_directory + break + if directory is None: + raise Error(f'No glyphs directory is mapped to "{layer_name}".') + return directory + def __contains__(self, name: str) -> bool: return name in self._layers