From c7823a2c46db6cc485bd8435ef148c4a361e46e1 Mon Sep 17 00:00:00 2001 From: Jany Belluz Date: Tue, 12 Dec 2017 18:59:53 +0000 Subject: [PATCH] Sort out the userData/lib stuff + other fixes --- Lib/glyphsLib/builder/__init__.py | 32 ++- Lib/glyphsLib/builder/builders.py | 128 +++++++--- Lib/glyphsLib/builder/font.py | 4 +- Lib/glyphsLib/builder/glyph.py | 4 +- Lib/glyphsLib/builder/instances.py | 24 +- Lib/glyphsLib/builder/masters.py | 22 +- Lib/glyphsLib/builder/user_data.py | 135 +++++++---- Lib/glyphsLib/classes.py | 17 +- Lib/glyphsLib/designSpaceDocument.py | 56 ++++- tests/interpolation_test.py | 6 - tests/lib_and_user_data.png | Bin 0 -> 51696 bytes tests/lib_and_user_data.uml | 96 ++++++++ tests/lib_and_user_data_test.py | 245 ++++++++++++++++++++ tests/run_various_tests_on_various_files.py | 2 - tests/test_helpers.py | 11 +- tests/writer_test.py | 1 + tox.ini | 1 + 17 files changed, 664 insertions(+), 120 deletions(-) create mode 100644 tests/lib_and_user_data.png create mode 100644 tests/lib_and_user_data.uml create mode 100644 tests/lib_and_user_data_test.py diff --git a/Lib/glyphsLib/builder/__init__.py b/Lib/glyphsLib/builder/__init__.py index 5bcdf549b..e27ca485e 100644 --- a/Lib/glyphsLib/builder/__init__.py +++ b/Lib/glyphsLib/builder/__init__.py @@ -25,8 +25,12 @@ logger = logging.getLogger(__name__) -def to_ufos(font, include_instances=False, family_name=None, - propagate_anchors=True, ufo_module=defcon): +def to_ufos(font, + include_instances=False, + family_name=None, + propagate_anchors=True, + ufo_module=defcon, + minimize_glyphs_diffs=False): # TODO: (jany) Update documentation """Take .glyphs file data and load it into UFOs. @@ -42,7 +46,8 @@ def to_ufos(font, include_instances=False, family_name=None, font, ufo_module=ufo_module, family_name=family_name, - propagate_anchors=propagate_anchors) + propagate_anchors=propagate_anchors, + minimize_glyphs_diffs=minimize_glyphs_diffs) result = list(builder.masters) @@ -51,8 +56,11 @@ def to_ufos(font, include_instances=False, family_name=None, return result -def to_designspace(font, family_name=None, propagate_anchors=True, - ufo_module=defcon): +def to_designspace(font, + family_name=None, + propagate_anchors=True, + ufo_module=defcon, + minimize_glyphs_diffs=False): # TODO: (jany) Update documentation """Take .glyphs file data and load it into a Designspace Document + UFOS. @@ -68,11 +76,15 @@ def to_designspace(font, family_name=None, propagate_anchors=True, font, ufo_module=ufo_module, family_name=family_name, - propagate_anchors=propagate_anchors) + propagate_anchors=propagate_anchors, + use_designspace=True, + minimize_glyphs_diffs=minimize_glyphs_diffs) return builder.designspace -def to_glyphs(ufos_or_designspace, glyphs_module=classes): +def to_glyphs(ufos_or_designspace, + glyphs_module=classes, + minimize_ufo_diffs=False): """ Take a list of UFOs and combine them into a single .glyphs file. @@ -83,8 +95,10 @@ def to_glyphs(ufos_or_designspace, glyphs_module=classes): # FIXME: (jany) duck-type instead of isinstance if isinstance(ufos_or_designspace, DesignSpaceDocument): builder = GlyphsBuilder(designspace=ufos_or_designspace, - glyphs_module=glyphs_module) + glyphs_module=glyphs_module, + minimize_ufo_diffs=minimize_ufo_diffs) else: builder = GlyphsBuilder(ufos=ufos_or_designspace, - glyphs_module=glyphs_module) + glyphs_module=glyphs_module, + minimize_ufo_diffs=minimize_ufo_diffs) return builder.font diff --git a/Lib/glyphsLib/builder/builders.py b/Lib/glyphsLib/builder/builders.py index a58bbfbad..7bee6f89b 100644 --- a/Lib/glyphsLib/builder/builders.py +++ b/Lib/glyphsLib/builder/builders.py @@ -51,7 +51,9 @@ def __init__(self, ufo_module=defcon, designspace_module=designSpaceDocument, family_name=None, - propagate_anchors=True): + propagate_anchors=True, + use_designspace=False, + minimize_glyphs_diffs=False): """Create a builder that goes from Glyphs to UFO + designspace. Keyword arguments: @@ -64,10 +66,18 @@ def __init__(self, family_name -- if provided, the master UFOs will be given this name and only instances with this name will be returned. propagate_anchors -- set to False to prevent anchor propagation + use_designspace -- set to True to make optimal use of the designspace: + data that is common to all ufos will go there. + minimize_glyphs_diffs -- set to True to store extra info in UFOs + in order to get smaller diffs between .glyphs + .glyphs files when going glyphs->ufo->glyphs. """ self.font = font self.ufo_module = ufo_module self.designspace_module = designspace_module + self.propagate_anchors = propagate_anchors + self.use_designspace = use_designspace + self.minimize_glyphs_diffs = minimize_glyphs_diffs # The set of UFOs (= defcon.Font objects) that will be built, # indexed by master ID, the same order as masters in the source GSFont. @@ -100,8 +110,6 @@ def __init__(self, else: self._instance_family_name = family_name - self.propagate_anchors = propagate_anchors - @property def masters(self): """Get an iterator over master UFOs that match the given family_name. @@ -137,8 +145,8 @@ def masters(self): if assoc_id != layer.layerId: # Store all layers, even the invalid ones, and just skip # them and print a warning below. - supplementary_layer_data.append( - (assoc_id, glyph_name, layer_name, layer)) + supplementary_layer_data.append((assoc_id, glyph_name, + layer_name, layer)) continue ufo = self._ufos[layer_id] @@ -150,14 +158,14 @@ def masters(self): for master_id, glyph_name, layer_name, layer \ in supplementary_layer_data: - if (layer.layerId not in master_layer_ids and - layer.associatedMasterId not in master_layer_ids): + if (layer.layerId not in master_layer_ids + and layer.associatedMasterId not in master_layer_ids): self.logger.warn( '{}, glyph "{}": Layer "{}" is dangling and will be ' 'skipped. Did you copy a glyph from a different font? If ' 'so, you should clean up any phantom layers not associated ' - 'with an actual master.'.format( - self.font.familyName, glyph_name, layer.layerId)) + 'with an actual master.'.format(self.font.familyName, + glyph_name, layer.layerId)) continue if not layer_name: @@ -184,6 +192,8 @@ def masters(self): self.to_ufo_propagate_font_anchors(ufo) self.to_ufo_features(ufo) # This depends on the glyphOrder key self.to_ufo_kerning_groups(ufo, kerning_groups) + for layer in ufo.layers: + self.to_ufo_layer_lib(layer) for master_id, kerning in self.font.kerning.items(): self.to_ufo_kerning(self._ufos[master_id], kerning) @@ -215,7 +225,8 @@ def designspace(self): self._designspace = self.designspace_module.DesignSpaceDocument( writerClass=designSpaceDocument.InMemoryDocWriter, fontClass=self.ufo_module.Font) - self.to_ufo_instances() + self.to_designspace_instances() + self.to_designspace_family_user_data() return self._designspace # DEPRECATED @@ -252,15 +263,16 @@ def instance_data(self): from .glyph import to_ufo_glyph, to_ufo_glyph_background from .guidelines import to_ufo_guidelines from .hints import to_ufo_hints - from .instances import to_ufo_instances + from .instances import to_designspace_instances from .kerning import (to_ufo_kerning, to_ufo_glyph_groups, to_ufo_kerning_groups) from .masters import to_ufo_master_attributes from .names import to_ufo_names from .paths import to_ufo_paths - from .user_data import (to_ufo_family_user_data, to_ufo_master_user_data, - to_ufo_glyph_user_data, to_ufo_layer_user_data, - to_ufo_node_user_data) + from .user_data import (to_designspace_family_user_data, + to_ufo_family_user_data, to_ufo_master_user_data, + to_ufo_glyph_user_data, to_ufo_layer_lib, + to_ufo_layer_user_data, to_ufo_node_user_data) def filter_instances_by_family(instances, family_name=None): @@ -280,7 +292,11 @@ def filter_instances_by_family(instances, family_name=None): class GlyphsBuilder(_LoggerMixin): """Builder for UFO + designspace to Glyphs.""" - def __init__(self, ufos=[], designspace=None, glyphs_module=classes): + def __init__(self, + ufos=[], + designspace=None, + glyphs_module=classes, + minimize_ufo_diffs=False): """Create a builder that goes from UFOs + designspace to Glyphs. Keyword arguments: @@ -292,7 +308,13 @@ def __init__(self, ufos=[], designspace=None, glyphs_module=classes): instances of your own classes, or pass the Glyphs.app module that holds the official classes to import UFOs into Glyphs.app) + minimize_ufo_diffs -- set to True to store extra info in .glyphs files + in order to get smaller diffs between UFOs + when going UFOs->glyphs->UFOs """ + self.glyphs_module = glyphs_module + self.minimize_ufo_diffs = minimize_ufo_diffs + if designspace is not None: self.designspace = designspace if ufos: @@ -301,18 +323,17 @@ def __init__(self, ufos=[], designspace=None, glyphs_module=classes): else: self.ufos = [] for source in designspace.sources: - try: - # It's an in-memory source descriptor - self.ufos.append(source.font) - except AttributeError: - self.ufos.append(designspace.fontClass(source.path)) + # FIXME: (jany) Do something better for the InMemory stuff + # Is it an in-memory source descriptor? + if not hasattr(source, 'font'): + source.font = designspace.fontClass(source.path) + self.ufos.append(source.font) elif ufos: - self.designspace = None + self.designspace = self._fake_designspace(ufos) self.ufos = ufos else: raise RuntimeError( 'Please provide a designspace or at least one UFO.') - self.glyphs_module = glyphs_module self._font = None """The GSFont that will be built.""" @@ -337,6 +358,7 @@ def font(self): self._font.masters.insert(len(self._font.masters), master) for layer in ufo.layers: + self.to_glyphs_layer_lib(layer) for glyph in layer: self.to_glyphs_glyph(glyph, layer, master) self.to_glyphs_glyph_groups(kerning_groups, glyph) @@ -344,22 +366,66 @@ def font(self): self.to_glyphs_kerning(ufo, master) # Now that all GSGlyph are built, restore the glyph order - first_ufo = next(iter(self.ufos)) - if GLYPH_ORDER_KEY in first_ufo.lib: - glyph_order = first_ufo.lib[GLYPH_ORDER_KEY] - lookup = {name: i for i, name in enumerate(glyph_order)} - self.font.glyphs = sorted( - self.font.glyphs, - key=lambda glyph: lookup.get(glyph.name, 1 << 63)) + for first_ufo in self.ufos: + if GLYPH_ORDER_KEY in first_ufo.lib: + glyph_order = first_ufo.lib[GLYPH_ORDER_KEY] + lookup = {name: i for i, name in enumerate(glyph_order)} + self.font.glyphs = sorted( + self.font.glyphs, + key=lambda glyph: lookup.get(glyph.name, 1 << 63)) + + # FIXME: (jany) Only do that on the first one. Maybe we should + # merge the various `public.glyphorder` values? + break # Restore the layer ordering in each glyph for glyph in self._font.glyphs: self.to_glyphs_layer_order(glyph) + self.to_glyphs_family_user_data_from_designspace() self.to_glyphs_instances() return self._font + def _fake_designspace(self, ufos): + """Build a fake designspace with the given UFOs as sources, so that all + builder functions can rely on the presence of a designspace. + """ + designspace = designSpaceDocument.DesignSpaceDocument( + writerClass=designSpaceDocument.InMemoryDocWriter) + + for ufo in ufos: + source = designspace.newSourceDescriptor() + source.font = ufo + source.familyName = ufo.info.familyName + source.styleName = ufo.info.styleName + # source.name = '%s %s' % (source.familyName, source.styleName) + source.path = ufo.path + + # MutatorMath.DesignSpaceDocumentWriter iterates over the location + # dictionary, which is non-deterministic so it can cause test failures. + # We therefore use an OrderedDict to which we insert in axis order. + # Since glyphsLib will switch to DesignSpaceDocument once that is + # integrated into fonttools, it's not worth fixing upstream. + # https://github.com/googlei18n/glyphsLib/issues/165 + # FIXME: (jany) still needed? + # location = OrderedDict() + # for axis in self.designspace.axes: + # value_key = axis.name + 'Value' + # if axis.name.startswith('custom'): + # # FIXME: (jany) this is getting boring + # value_key = 'customValue' + axis.name[len('custom'):] + # location[axis.name] = ufo.lib.get( + # MASTER_CUSTOM_PARAM_PREFIX + value_key, DEFAULT_LOCS[axis.name]) + source.location = {} + # if font is regular: + # source.copyLib = True + # source.copyInfo = True + # source.copyGroups = True + # source.copyFeatures = True + designspace.addSource(source) + return designspace + # Implementation is split into one file per feature from .anchors import to_glyphs_glyph_anchors from .annotations import to_glyphs_annotations @@ -380,8 +446,10 @@ def font(self): from .masters import to_glyphs_master_attributes from .names import to_glyphs_family_names, to_glyphs_master_names from .paths import to_glyphs_paths - from .user_data import (to_glyphs_family_user_data, + from .user_data import (to_glyphs_family_user_data_from_designspace, + to_glyphs_family_user_data_from_ufo, to_glyphs_master_user_data, to_glyphs_glyph_user_data, + to_glyphs_layer_lib, to_glyphs_layer_user_data, to_glyphs_node_user_data) diff --git a/Lib/glyphsLib/builder/font.py b/Lib/glyphsLib/builder/font.py index a91a7e388..f6b8f67c1 100644 --- a/Lib/glyphsLib/builder/font.py +++ b/Lib/glyphsLib/builder/font.py @@ -140,7 +140,7 @@ def _set_glyphs_font_attributes(self, ufo): font.manufacturerURL = info.openTypeNameManufacturerURL self.to_glyphs_family_names(ufo) - self.to_glyphs_family_user_data(ufo) + self.to_glyphs_family_user_data_from_ufo(ufo) self.to_glyphs_custom_params(ufo, font) self.to_glyphs_features(ufo) @@ -151,6 +151,8 @@ def to_glyphs_ordered_masters(self): def _original_master_order(ufo): + # FIXME: (jany) Here we should rely on order of sources in designspace + # if self.use_designspace try: return ufo.lib[MASTER_ORDER_LIB_KEY] except KeyError: diff --git a/Lib/glyphsLib/builder/glyph.py b/Lib/glyphsLib/builder/glyph.py index e6bde7799..5cb36ce0a 100644 --- a/Lib/glyphsLib/builder/glyph.py +++ b/Lib/glyphsLib/builder/glyph.py @@ -112,7 +112,7 @@ def to_ufo_glyph(self, ufo_glyph, layer, glyph): self.to_ufo_glyph_background(ufo_glyph, layer) self.to_ufo_annotations(ufo_glyph, layer) self.to_ufo_hints(ufo_glyph, layer) - self.to_ufo_glyph_user_data(ufo_glyph, glyph) + self.to_ufo_glyph_user_data(ufo_glyph.font, glyph) self.to_ufo_layer_user_data(ufo_glyph, layer) self.to_ufo_smart_component_axes(ufo_glyph, glyph) @@ -211,7 +211,7 @@ def to_glyphs_glyph(self, ufo_glyph, ufo_layer, master): self.to_glyphs_guidelines(ufo_glyph, layer) self.to_glyphs_annotations(ufo_glyph, layer) self.to_glyphs_hints(ufo_glyph, layer) - self.to_glyphs_glyph_user_data(ufo_glyph, glyph) + self.to_glyphs_glyph_user_data(ufo_glyph.font, glyph) self.to_glyphs_layer_user_data(ufo_glyph, layer) self.to_glyphs_smart_component_axes(ufo_glyph, glyph) diff --git a/Lib/glyphsLib/builder/instances.py b/Lib/glyphsLib/builder/instances.py index b768e647b..b9fd50857 100644 --- a/Lib/glyphsLib/builder/instances.py +++ b/Lib/glyphsLib/builder/instances.py @@ -32,7 +32,7 @@ INSTANCE_INTERPOLATIONS_KEY = GLYPHS_PREFIX + 'intanceInterpolations' -def to_ufo_instances(self): +def to_designspace_instances(self): """Write instance data from self.font to self.designspace.""" # base_family = masters[0].info.familyName @@ -48,18 +48,20 @@ def to_ufo_instances(self): # instances = list(filter(is_instance_active, instance_data.get('data', []))) ufo_masters = list(self.masters) - varfont_origin = _get_varfont_origin(ufo_masters) - regular = _find_regular_master(ufo_masters, regularName=varfont_origin) - _to_ufo_designspace_axes(self, regular) - _to_ufo_designspace_sources(self, regular) + if ufo_masters: + varfont_origin = _get_varfont_origin(ufo_masters) + regular = _find_regular_master(ufo_masters, regularName=varfont_origin) + _to_designspace_axes(self, regular) + _to_designspace_sources(self, regular) for instance in self.font.instances: - _to_ufo_designspace_instance(self, instance) + _to_designspace_instance(self, instance) def _get_varfont_origin(masters): # the 'Variation Font Origin' is a font-wide custom parameter, thus it is # shared by all the master ufos; here we just get it from the first one + assert len(masters) > 0 varfont_origin_key = "Variation Font Origin" return masters[0].lib.get(FONT_CUSTOM_PARAM_PREFIX + varfont_origin_key) @@ -145,7 +147,7 @@ def find_base_style(masters): } -def _to_ufo_designspace_axes(self, regular_master): +def _to_designspace_axes(self, regular_master): # According to Georg Seifert, Glyphs 3 will have a better model # for describing variation axes. The plan is to store the axis # information globally in the Glyphs file. In addition to actual @@ -220,7 +222,7 @@ def _to_ufo_designspace_axes(self, regular_master): self.designspace.addAxis(axis) -def _to_ufo_designspace_sources(self, regular): +def _to_designspace_sources(self, regular): """Add master UFOs to the designspace document.""" # FIXME: (jany) maybe read data from the GSFontMasters directly? for master, font in zip(self.font.masters, self.masters): @@ -229,9 +231,9 @@ def _to_ufo_designspace_sources(self, regular): source.familyName = font.info.familyName source.styleName = font.info.styleName source.name = '%s %s' % (source.familyName, source.styleName) - try: + if UFO_FILENAME_KEY in master.userData: source.filename = master.userData[UFO_FILENAME_KEY] - except KeyError: + else: source.filename = build_ufo_path('.', source.familyName, source.styleName) @@ -259,7 +261,7 @@ def _to_ufo_designspace_sources(self, regular): self.designspace.addSource(source) -def _to_ufo_designspace_instance(self, instance): +def _to_designspace_instance(self, instance): ufo_instance = self.designspace.newInstanceDescriptor() for p in instance.customParameters: param, value = p.name, p.value diff --git a/Lib/glyphsLib/builder/masters.py b/Lib/glyphsLib/builder/masters.py index ae18039cc..12c519e3d 100644 --- a/Lib/glyphsLib/builder/masters.py +++ b/Lib/glyphsLib/builder/masters.py @@ -70,7 +70,8 @@ def to_ufo_master_attributes(self, ufo, master): self.to_ufo_custom_params(ufo, master) master_id = master.id - ufo.lib[MASTER_ID_LIB_KEY] = master_id + if self.minimize_glyphs_diffs: + ufo.lib[MASTER_ID_LIB_KEY] = master_id def to_glyphs_master_attributes(self, ufo, master): @@ -79,7 +80,7 @@ def to_glyphs_master_attributes(self, ufo, master): except KeyError: master.id = str(uuid.uuid4()) - if ufo.path: + if ufo.path and self.minimize_ufo_diffs: master.userData[UFO_FILENAME_KEY] = os.path.basename(ufo.path) master.ascender = ufo.info.ascender @@ -102,6 +103,20 @@ def to_glyphs_master_attributes(self, ufo, master): # Retrieve the master locations: weight, width, custom 0 - 1 - 2 - 3 source = _get_designspace_source_for_ufo(self, ufo) for axis in ['weight', 'width']: + # First, try the designspace + try: + # TODO: ??? name = source.lib[...] + # setattr(master, axis, name) + raise KeyError + except KeyError: + # Second, try the custom key + try: + setattr(master, axis, ufo.lib[GLYPHS_PREFIX + axis]) + except KeyError: + # FIXME: (jany) as last resort, use 400/700 as a location, + # from the weightClass/widthClass? + pass + value_key = axis + 'Value' # First, try the designspace try: @@ -110,7 +125,7 @@ def to_glyphs_master_attributes(self, ufo, master): except KeyError: # Second, try the custom key try: - setattr(master, value_key, ufo.lib[GLYPHS_PREFIX + axis]) + setattr(master, value_key, ufo.lib[GLYPHS_PREFIX + value_key]) except KeyError: # FIXME: (jany) as last resort, use 400/700 as a location, # from the weightClass/widthClass? @@ -151,6 +166,5 @@ def to_glyphs_master_attributes(self, ufo, master): def _get_designspace_source_for_ufo(self, ufo): for source in self.designspace.sources: - # FIXME: (jany) assumes InMemoryDesignSpaceDocument if source.font == ufo: return source diff --git a/Lib/glyphsLib/builder/user_data.py b/Lib/glyphsLib/builder/user_data.py index 190e49014..63d09a3bc 100644 --- a/Lib/glyphsLib/builder/user_data.py +++ b/Lib/glyphsLib/builder/user_data.py @@ -15,46 +15,60 @@ from __future__ import (print_function, division, absolute_import, unicode_literals) -from .constants import GLYPHS_PREFIX, PUBLIC_PREFIX +import base64 +import os +import posixpath -MASTER_USER_DATA_KEY = GLYPHS_PREFIX + 'fontMaster.userData' -LAYER_USER_DATA_KEY = GLYPHS_PREFIX + 'layer.userData' -GLYPH_USER_DATA_KEY = GLYPHS_PREFIX + 'glyph.userData' -NODE_USER_DATA_KEY = GLYPHS_PREFIX + 'node.userData' +from .constants import GLYPHS_PREFIX, GLYPHLIB_PREFIX, PUBLIC_PREFIX + +UFO_DATA_KEY = GLYPHLIB_PREFIX + 'ufoData' +FONT_USER_DATA_KEY = GLYPHLIB_PREFIX + 'fontUserData' +LAYER_LIB_KEY = GLYPHLIB_PREFIX + 'layerLib' +GLYPH_USER_DATA_KEY = GLYPHLIB_PREFIX + 'glyphUserData' +NODE_USER_DATA_KEY = GLYPHLIB_PREFIX + 'nodeUserData' + + +def to_designspace_family_user_data(self): + if self.use_designspace: + self.designspace.lib.update(dict(self.font.userData)) def to_ufo_family_user_data(self, ufo): """Set family-wide user data as Glyphs does.""" - user_data = self.font.userData - for key in user_data.keys(): - # FIXME: (jany) Should put a Glyphs prefix? - # FIXME: (jany) At least identify which stuff we have put in lib during - # the Glyphs->UFO so that we don't take it back into userData in - # the other direction. - ufo.lib[key] = user_data[key] + if not self.use_designspace: + ufo.lib[FONT_USER_DATA_KEY] = dict(self.font.userData) def to_ufo_master_user_data(self, ufo, master): """Set master-specific user data as Glyphs does.""" - user_data = master.userData - if user_data: - data = {} - for key in user_data.keys(): - data[key] = user_data[key] - ufo.lib[MASTER_USER_DATA_KEY] = data + for key in master.userData.keys(): + if _user_data_has_no_special_meaning(key): + ufo.lib[key] = master.userData[key] + # Restore UFO data files + if UFO_DATA_KEY in master.userData: + for filename, data in master.userData[UFO_DATA_KEY].items(): + os_filename = os.path.join(*filename.split('/')) + ufo.data[os_filename] = base64.b64decode(data) -def to_ufo_glyph_user_data(self, ufo_glyph, glyph): - user_data = glyph.userData - if user_data: - ufo_glyph.lib[GLYPH_USER_DATA_KEY] = dict(user_data) + +def to_ufo_glyph_user_data(self, ufo, glyph): + key = GLYPH_USER_DATA_KEY + '.' + glyph.name + if glyph.userData: + ufo.lib[key] = dict(glyph.userData) + + +def to_ufo_layer_lib(self, ufo_layer): + key = LAYER_LIB_KEY + '.' + ufo_layer.name + if key in self.font.userData.keys(): + ufo_layer.lib = self.font.userData[key] def to_ufo_layer_user_data(self, ufo_glyph, layer): user_data = layer.userData - if user_data: - key = LAYER_USER_DATA_KEY + '.' + layer.layerId - ufo_glyph.lib[key] = dict(user_data) + for key in user_data.keys(): + if _user_data_has_no_special_meaning(key): + ufo_glyph.lib[key] = user_data[key] def to_ufo_node_user_data(self, ufo_glyph, node): @@ -65,32 +79,68 @@ def to_ufo_node_user_data(self, ufo_glyph, node): ufo_glyph.lib[key] = dict(user_data) -def to_glyphs_family_user_data(self, ufo): - """Set the GSFont userData from the UFO family-wide user data.""" +def to_glyphs_family_user_data_from_designspace(self): + """Set the GSFont userData from the designspace family-wide lib data.""" target_user_data = self.font.userData - for key, value in ufo.lib.items(): - if _user_data_was_originally_there_family_wide(key): + for key, value in self.designspace.lib.items(): + if _user_data_has_no_special_meaning(key): target_user_data[key] = value +def to_glyphs_family_user_data_from_ufo(self, ufo): + """Set the GSFont userData from the UFO family-wide lib data.""" + target_user_data = self.font.userData + try: + for key, value in ufo.lib[FONT_USER_DATA_KEY].items(): + # Existing values taken from the designspace lib take precedence + if key not in target_user_data.keys(): + target_user_data[key] = value + except KeyError: + # No FONT_USER_DATA in ufo.lib + pass + + def to_glyphs_master_user_data(self, ufo, master): - """Set the GSFontMaster userData from the UFO master-specific user data.""" - if MASTER_USER_DATA_KEY not in ufo.lib: - return - user_data = ufo.lib[MASTER_USER_DATA_KEY] - if user_data: - master.userData = user_data + """Set the GSFontMaster userData from the UFO master-specific lib data.""" + target_user_data = master.userData + for key, value in ufo.lib.items(): + if _user_data_has_no_special_meaning(key): + target_user_data[key] = value + + # Save UFO data files + if ufo.data.fileNames: + ufo_data = {} + for os_filename in ufo.data.fileNames: + filename = posixpath.join(*os_filename.split(os.path.sep)) + data_bytes = base64.b64encode(ufo.data[os_filename]) + # FIXME: (jany) The `decode` is here because putting bytes in + # userData doesn't work in Python 3. (comes out as `"b'stuff'"`) + ufo_data[filename] = data_bytes.decode() + master.userData[UFO_DATA_KEY] = ufo_data -def to_glyphs_glyph_user_data(self, ufo_glyph, glyph): - if GLYPH_USER_DATA_KEY in ufo_glyph.lib: - glyph.userData = ufo_glyph.lib[GLYPH_USER_DATA_KEY] +def to_glyphs_glyph_user_data(self, ufo, glyph): + key = GLYPH_USER_DATA_KEY + '.' + glyph.name + if key in ufo.lib: + glyph.userData = ufo.lib[key] + + +def to_glyphs_layer_lib(self, ufo_layer): + user_data = {} + for key, value in ufo_layer.lib.items(): + if _user_data_has_no_special_meaning(key): + user_data[key] = value + + if user_data: + key = LAYER_LIB_KEY + '.' + ufo_layer.name + self.font.userData[key] = user_data def to_glyphs_layer_user_data(self, ufo_glyph, layer): - key = LAYER_USER_DATA_KEY + '.' + layer.layerId - if key in ufo_glyph.lib: - layer.userData = ufo_glyph.lib[key] + user_data = layer.userData + for key, value in ufo_glyph.lib.items(): + if _user_data_has_no_special_meaning(key): + user_data[key] = value def to_glyphs_node_user_data(self, ufo_glyph, node): @@ -100,6 +150,5 @@ def to_glyphs_node_user_data(self, ufo_glyph, node): node.userData = ufo_glyph.lib[key] -def _user_data_was_originally_there_family_wide(key): - # FIXME: (jany) Identify better which keys must be brought back? +def _user_data_has_no_special_meaning(key): return not (key.startswith(GLYPHS_PREFIX) or key.startswith(PUBLIC_PREFIX)) diff --git a/Lib/glyphsLib/classes.py b/Lib/glyphsLib/classes.py index ff32230a9..697371fd6 100644 --- a/Lib/glyphsLib/classes.py +++ b/Lib/glyphsLib/classes.py @@ -443,7 +443,8 @@ def values(self): def append(self, FontMaster): FontMaster.font = self._owner - FontMaster.id = str(uuid.uuid4()).upper() + if not FontMaster.id: + FontMaster.id = str(uuid.uuid4()).upper() self._owner._masters.append(FontMaster) # Cycle through all glyphs and append layer @@ -984,6 +985,8 @@ class UserDataProxy(Proxy): def __getitem__(self, key): if self._owner._userData is None: raise KeyError + # This is not the normal `dict` behaviour, because this does not raise + # `KeyError` and instead just returns `None`. It matches Glyphs.app. return self._owner._userData.get(key) def __setitem__(self, key, value): @@ -1004,6 +1007,8 @@ def __contains__(self, item): def __iter__(self): if self._owner._userData is None: return + # This is not the normal `dict` behaviour, because this yields values + # instead of keys. It matches Glyphs.app though. Urg. for value in self._owner._userData.values(): yield value @@ -1116,8 +1121,8 @@ def setValue(self, value): class GSAlignmentZone(GSBase): - def __init__(self, pos=0, size=20): + super(GSAlignmentZone, self).__init__() self.position = pos self.size = size @@ -1276,6 +1281,8 @@ def shouldWriteValueForKey(self, key): @property def name(self): + # FIXME: (jany) this getter looks stupid, it never returns the value + # from self._name. TODO: test what Glyphs does and how this makes sense name = self.customParameters["Master Name"] if name is None: names = [self.weight, self.width] @@ -1301,7 +1308,7 @@ def name(self, value): # because during init it sets an empty string if value: self._name = value - self.customParameters["Master Name"] = value + # self.customParameters["Master Name"] = value customParameters = property( lambda self: CustomParametersProxy(self), @@ -1325,6 +1332,7 @@ class GSNode(GSBase): def __init__(self, position=(0, 0), nodetype=LINE, smooth=False, name=None): + super(GSNode, self).__init__() self.position = Point(position[0], position[1]) self.type = nodetype self.smooth = smooth @@ -1487,6 +1495,7 @@ class GSPath(GSBase): _parent = None def __init__(self): + super(GSPath, self).__init__() self._closed = True self.nodes = [] @@ -2784,7 +2793,7 @@ def shouldWriteValueForKey(self, key): lambda self, value: GlyphLayerProxy(self).setter(value)) def _setupLayer(self, layer, key): - assert type(key) == str + assert isinstance(key, (str, unicode)) layer.parent = self layer.layerId = key # TODO use proxy `self.parent.masters[key]` diff --git a/Lib/glyphsLib/designSpaceDocument.py b/Lib/glyphsLib/designSpaceDocument.py index c89f38d21..e4cce59f6 100644 --- a/Lib/glyphsLib/designSpaceDocument.py +++ b/Lib/glyphsLib/designSpaceDocument.py @@ -43,6 +43,10 @@ def __str__(self): return repr(self.msg) + repr(self.obj) +class NoFontError(DesignSpaceDocumentError): + """Raised when a SourceDescriptor cannot be linked to a source UFO.""" + + def _indent(elem, whitespace=" ", level=0): # taken from http://effbot.org/zone/element-lib.htm#prettyprint i = "\n" + level * whitespace @@ -82,6 +86,7 @@ class SourceDescriptor(SimpleDescriptor): 'familyName', 'styleName'] def __init__(self): + self.document = None # a reference to the parent document self.filename = None # the original path as found in the document self.path = None # the absolute path, calculated from filename self.name = None @@ -332,6 +337,10 @@ def write(self, pretty=True): self.root.append(ET.Element("instances")) for instanceObject in self.documentObject.instances: self._addInstance(instanceObject) + + if self.documentObject.lib: + self._addLib(self.documentObject.lib) + if pretty: _indent(self.root, whitespace=self._whiteSpace) tree = ET.ElementTree(self.root) @@ -537,6 +546,12 @@ def _addSource(self, sourceObject): sourceElement.append(locationElement) self.root.findall('.sources')[0].append(sourceElement) + def _addLib(self, dict): + libElement = ET.Element('lib') + # TODO: (jany) PLIST I guess? + libElement.text = json.dumps(dict) + self.root.append(libElement) + def _writeGlyphElement(self, instanceElement, instanceObject, glyphName, data): glyphElement = ET.Element('glyph') if data.get('mute'): @@ -588,6 +603,7 @@ def read(self): self.readRules() self.readSources() self.readInstances() + self.readLib() def getSourcePaths(self, makeGlyphs=True, makeKerning=True, makeInfo=True): paths = [] @@ -771,7 +787,7 @@ def readSources(self): for kerningElement in sourceElement.findall(".kerning"): if kerningElement.attrib.get('mute') == '1': sourceObject.muteKerning = True - self.documentObject.sources.append(sourceObject) + self.documentObject.addSource(sourceObject) def locationFromElement(self, element): elementLocation = None @@ -961,6 +977,12 @@ def readGlyphElement(self, glyphElement, instanceObject): glyphData['masters'] = glyphSources instanceObject.glyphs[glyphName] = glyphData + def readLib(self): + """ TODO: (jany) doc + """ + for libElement in self.root.findall(".lib"): + self.documentObject.lib = json.loads(libElement.text) + class DesignSpaceDocument(object): """ Read, write data from the designspace file""" @@ -974,6 +996,7 @@ def __init__(self, readerClass=None, writerClass=None, fontClass=None): self.rules = [] self.default = None # name of the default master self.defaultLoc = None + self.lib = {} # if readerClass is not None: self.readerClass = readerClass @@ -1046,7 +1069,7 @@ def updatePaths(self): """ - for descriptor in self.sources + self.instances: + for descriptor in list(self.sources) + self.instances: # check what the relative path really should be? expectedFilename = None if descriptor.path is not None and self.path is not None: @@ -1062,8 +1085,24 @@ def updatePaths(self): if descriptor.filename is not expectedFilename: descriptor.filename = expectedFilename + @property + def sources(self): + # Return an immutable list to force users to call `addSource` + # or the setter. This is because I want source descriptors to keep a + # reference to their parent for their `font` property. + # Maybe this is all too much and another design is needed + # (where source descriptors don't instanciate fonts) + return tuple(self._sources) + + @sources.setter + def sources(self, sources): + self._sources = list(sources) + for source in self._sources: + source.document = self + def addSource(self, sourceDescriptor): - self.sources.append(sourceDescriptor) + sourceDescriptor.document = self + self._sources.append(sourceDescriptor) def addInstance(self, instanceDescriptor): self.instances.append(instanceDescriptor) @@ -1369,9 +1408,14 @@ def font(self): if self._font is not None: return self._font - # FIXME: (jany) will there always be a path? if self.path: self._font = self.fontClass(self.path) + elif self.document and self.filename: + path = os.path.join(os.path.dirname(self.document), self.filename) + self._font = self.fontClass(path) + + if self._font is None: + raise NoFontError("") return self._font @@ -1400,6 +1444,10 @@ class InMemoryDocWriter(BaseDocWriter): @classmethod def getSourceDescriptor(cls, document): + # FIXME: (jany) settle on whether we want + # 1. descriptors to hold the fontClass + # 2. descriptors to refer to their parent document + # 3. another design (back to "dumb data bag" descriptors, no OOP) return cls.sourceDescriptorClass(fontClass=document.fontClass) def write(self, pretty=True): diff --git a/tests/interpolation_test.py b/tests/interpolation_test.py index 46adf2942..63aa75969 100644 --- a/tests/interpolation_test.py +++ b/tests/interpolation_test.py @@ -120,12 +120,6 @@ def expect_designspace(self, masters, instances, expectedFile): expectedPath = os.path.join(path, "data", expectedFile) with open(expectedPath, mode="r", encoding="utf-8") as f: expected = f.readlines() - if os.path.sep == '\\': - # On windows, the test must not fail because of a difference between - # forward and backward slashes in filname paths. - # The failure happens because of line 217 of "mutatorMath\ufo\document.py" - # > pathRelativeToDocument = os.path.relpath(fileName, os.path.dirname(self.path)) - expected = [line.replace('filename="out/', 'filename="out\\') for line in expected] if actual != expected: for line in difflib.unified_diff( expected, actual, diff --git a/tests/lib_and_user_data.png b/tests/lib_and_user_data.png new file mode 100644 index 0000000000000000000000000000000000000000..7e7b40e90409b6d95a9a731a44bb180f4527d6e3 GIT binary patch literal 51696 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%0A@ZN7649K${SRX8_;*=MmA6BNG7^L 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 S2o?N9cS}X%de&9rC;tb^@x?Fz literal 0 HcmV?d00001 diff --git a/tests/lib_and_user_data.uml b/tests/lib_and_user_data.uml new file mode 100644 index 000000000..618a012e2 --- /dev/null +++ b/tests/lib_and_user_data.uml @@ -0,0 +1,96 @@ +@startuml + +title + Relationships between the various Designspace/UFO ""lib"" fields + and their Glyphs.app ""userData"" counterparts + +end title + +skinParam { + ClassArrowThickness 2 +} + +package Designspace { + class DesignSpaceDocument { + + lib + } +} + +package UFO { + class Font { + + lib + + data + } + DesignSpaceDocument o-- "*" Font + + class Layer { + + lib + } + Font *-- "*" Layer + + class Glyph { + + lib + } + Layer *-- "*" Glyph +} + +package Glyphs.app { + class GSFont { + + userData + } + + class GSFontMaster { + + userData + } + GSFont *-- "*" GSFontMaster + + class GSGlyph { + + userData + } + GSFont *--- "*" GSGlyph + + class GSLayer { + + userData + } + GSGlyph *-- "*" GSLayer + + class GSNode { + + userData + } + GSLayer *-- "*" GSNode +} + + +DesignSpaceDocument "1" <-[#green]> "1" GSFont +note on link + Green arrows represent a 1-to-1 + mapping between Glyphs.app and + UFO/Designspace. In those cases, + the ""lib"" keys will be copied as-is + into ""userData"" and reciprocally. +end note + +Font "1" <-[#green]> "1" GSFontMaster +note on link + The UFO ""data"" will be stored + under a special key in the + masters' ""userData"". +end note + +Layer "*" .up[#blue].> "1" GSFontMaster +Font "1" <.[#blue]. "*" GSGlyph + +Glyph "1" <-[#green]> "1" GSLayer + +Glyph "1" <.[#blue]. "*" GSNode +note bottom on link + Blue arrows mean that there is no + 1-to-1 relationship between the two + worlds, so we store one side into a + special key on the other side. + + Here the GSNode ""userData"" is + stored into a special GLIF ""lib"" key. +end note + +@enduml diff --git a/tests/lib_and_user_data_test.py b/tests/lib_and_user_data_test.py new file mode 100644 index 000000000..f6ae70c4a --- /dev/null +++ b/tests/lib_and_user_data_test.py @@ -0,0 +1,245 @@ +# Copyright 2015 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import (print_function, division, absolute_import, + unicode_literals) + +import base64 +import os +import pytest + +import defcon +from glyphsLib import classes +from glyphsLib.builder.constants import GLYPHLIB_PREFIX +from glyphsLib.designSpaceDocument import (DesignSpaceDocument, + InMemoryDocWriter) + +from glyphsLib import to_glyphs, to_ufos, to_designspace + + +# GOAL: Test the translations between the various UFO lib and Glyphs userData. +# See the associated UML diagram: `lib_and_user_data.png` + + +def test_designspace_lib_equivalent_to_font_user_data(tmpdir): + designspace = DesignSpaceDocument(writerClass=InMemoryDocWriter) + designspace.lib['designspaceLibKey1'] = 'designspaceLibValue1' + + # Save to disk and reload the designspace to test the write/read of lib + path = os.path.join(str(tmpdir), 'test.designspace') + designspace.write(path) + designspace = DesignSpaceDocument(writerClass=InMemoryDocWriter) + designspace.read(path) + + font = to_glyphs(designspace) + + assert font.userData['designspaceLibKey1'] == 'designspaceLibValue1' + + designspace = to_designspace(font) + + assert designspace.lib['designspaceLibKey1'] == 'designspaceLibValue1' + + +def test_font_user_data_to_ufo_lib(): + # This happens only when not building a designspace + # Since there is no designspace.lib to store the font userData, + # the latter is duplicated in each output ufo + font = classes.GSFont() + font.masters.append(classes.GSFontMaster()) + font.masters.append(classes.GSFontMaster()) + font.userData['fontUserDataKey'] = 'fontUserDataValue' + + ufo1, ufo2 = to_ufos(font) + + assert ufo1.lib[GLYPHLIB_PREFIX + 'fontUserData'] == { + 'fontUserDataKey': 'fontUserDataValue' + } + assert ufo2.lib[GLYPHLIB_PREFIX + 'fontUserData'] == { + 'fontUserDataKey': 'fontUserDataValue' + } + + font = to_glyphs([ufo1, ufo2]) + + assert font.userData['fontUserDataKey'] == 'fontUserDataValue' + + +def test_ufo_lib_equivalent_to_font_master_user_data(): + ufo1 = defcon.Font() + ufo1.lib['ufoLibKey1'] = 'ufoLibValue1' + ufo2 = defcon.Font() + ufo2.lib['ufoLibKey2'] = 'ufoLibValue2' + + font = to_glyphs([ufo1, ufo2]) + + assert font.masters[0].userData['ufoLibKey1'] == 'ufoLibValue1' + assert font.masters[1].userData['ufoLibKey2'] == 'ufoLibValue2' + + ufo1, ufo2 = to_ufos(font) + + assert ufo1.lib['ufoLibKey1'] == 'ufoLibValue1' + assert ufo2.lib['ufoLibKey2'] == 'ufoLibValue2' + assert 'ufoLibKey2' not in ufo1.lib + assert 'ufoLibKey1' not in ufo2.lib + + +def test_ufo_data_into_font_master_user_data(tmpdir): + filename = os.path.join('org.customTool', 'ufoData.bin') + data = b'\x00\x01\xFF' + ufo = defcon.Font() + ufo.data[filename] = data + + font = to_glyphs([ufo]) + # Round-trip to disk for this one because I'm not sure there are other + # tests that read-write binary data + path = os.path.join(str(tmpdir), 'font.glyphs') + font.save(path) + font = classes.GSFont(path) + + # The path in the glyphs file should be os-agnostic (forward slashes) + assert font.masters[0].userData[GLYPHLIB_PREFIX + 'ufoData'] == { + # `decode`: not bytes in userData, only strings + 'org.customTool/ufoData.bin': base64.b64encode(data).decode() + } + + ufo, = to_ufos(font) + + assert ufo.data[filename] == data + + +def test_layer_lib_into_font_user_data(): + ufo = defcon.Font() + ufo.layers['public.default'].lib['layerLibKey1'] = 'layerLibValue1' + layer = ufo.newLayer('sketches') + layer.lib['layerLibKey2'] = 'layerLibValue2' + # layers won't roundtrip if they contain no glyph, except for the default + layer.newGlyph('bob') + + font = to_glyphs([ufo]) + + assert font.userData[GLYPHLIB_PREFIX + 'layerLib.public.default'] == { + 'layerLibKey1': 'layerLibValue1' + } + assert font.userData[GLYPHLIB_PREFIX + 'layerLib.sketches'] == { + 'layerLibKey2': 'layerLibValue2' + } + + ufo, = to_ufos(font) + + assert ufo.layers['public.default'].lib['layerLibKey1'] == 'layerLibValue1' + assert 'layerLibKey1' not in ufo.layers['sketches'].lib + assert ufo.layers['sketches'].lib['layerLibKey2'] == 'layerLibValue2' + assert 'layerLibKey2' not in ufo.layers['public.default'].lib + + +def test_glyph_user_data_into_ufo_lib(): + font = classes.GSFont() + font.masters.append(classes.GSFontMaster()) + glyph = classes.GSGlyph('a') + glyph.userData['glyphUserDataKey'] = 'glyphUserDataValue' + font.glyphs.append(glyph) + layer = classes.GSLayer() + layer.layerId = font.masters[0].id + glyph.layers.append(layer) + + ufo, = to_ufos(font) + + assert ufo.lib[GLYPHLIB_PREFIX + 'glyphUserData.a'] == { + 'glyphUserDataKey': 'glyphUserDataValue' + } + + font = to_glyphs([ufo]) + + assert font.glyphs['a'].userData[ + 'glyphUserDataKey'] == 'glyphUserDataValue' + + +def test_glif_lib_equivalent_to_layer_user_data(): + ufo = defcon.Font() + # This glyph is in the `public.default` layer + a = ufo.newGlyph('a') + a.lib['glifLibKeyA'] = 'glifLibValueA' + customLayer = ufo.newLayer('middleground') + # "a" is in both layers + customLayer.newGlyph('a') + # "b" is only in the second layer + b = customLayer.newGlyph('b') + b.lib['glifLibKeyB'] = 'glifLibValueB' + + font = to_glyphs([ufo]) + + for layer_id in font.glyphs['a'].layers.keys(): + layer = font.glyphs['a'].layers[layer_id] + if layer.layerId == font.masters[0].id: + default_layer = layer + else: + middleground = layer + assert default_layer.userData['glifLibKeyA'] == 'glifLibValueA' + assert 'glifLibKeyA' not in middleground.userData.keys() + + for layer_id in font.glyphs['b'].layers.keys(): + layer = font.glyphs['b'].layers[layer_id] + if layer.layerId == font.masters[0].id: + default_layer = layer + else: + middleground = layer + assert 'glifLibKeyB' not in default_layer.userData.keys() + assert middleground.userData['glifLibKeyB'] == 'glifLibValueB' + + ufo, = to_ufos(font) + + assert ufo['a'].lib['glifLibKeyA'] == 'glifLibValueA' + assert 'glifLibKeyA' not in ufo.layers['middleground']['a'] + assert ufo.layers['middleground']['b'].lib[ + 'glifLibKeyB'] == 'glifLibValueB' + + +def test_node_user_data_into_glif_lib(): + font = classes.GSFont() + master = classes.GSFontMaster() + master.id = "M1" + font.masters.append(master) + glyph = classes.GSGlyph('a') + layer = classes.GSLayer() + layer.layerId = "M1" + layer.associatedMasterId = "M1" + glyph.layers.append(layer) + font.glyphs.append(glyph) + path = classes.GSPath() + layer.paths.append(path) + node1 = classes.GSNode() + node1.userData['nodeUserDataKey1'] = 'nodeUserDataValue1' + node2 = classes.GSNode() + node2.userData['nodeUserDataKey2'] = 'nodeUserDataValue2' + path.nodes.append(classes.GSNode()) + path.nodes.append(node1) + path.nodes.append(classes.GSNode()) + path.nodes.append(classes.GSNode()) + path.nodes.append(node2) + + ufo, = to_ufos(font, minimize_glyphs_diffs=True) + + assert ufo['a'].lib[ + GLYPHLIB_PREFIX + 'nodeUserData.0.1'] == { + 'nodeUserDataKey1': 'nodeUserDataValue1' + } + assert ufo['a'].lib[ + GLYPHLIB_PREFIX + 'nodeUserData.0.4'] == { + 'nodeUserDataKey2': 'nodeUserDataValue2' + } + + font = to_glyphs([ufo]) + + path = font.glyphs['a'].layers['M1'].paths[0] + assert path.nodes[1].userData['nodeUserDataKey1'] == 'nodeUserDataValue1' + assert path.nodes[4].userData['nodeUserDataKey2'] == 'nodeUserDataValue2' diff --git a/tests/run_various_tests_on_various_files.py b/tests/run_various_tests_on_various_files.py index 2ed1154e6..2ca516ccb 100644 --- a/tests/run_various_tests_on_various_files.py +++ b/tests/run_various_tests_on_various_files.py @@ -134,8 +134,6 @@ def add_tests(cls, testable): 'classes': (GlyphsRT, GlyphsToDesignspaceRT), }, # The following contain .designspace files - # Actually both of these are UFO 2, so they creates lots of diffs - # TODO: find UFO 3 examples { 'name': 'spectral', 'git_url': 'https://github.com/productiontype/Spectral', diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 403b85f32..1638c95e2 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -88,6 +88,7 @@ def assertParseWriteRoundtrip(self, filename): class AssertUFORoundtrip(AssertLinesEqual): + """Check .glyphs -> UFOs + designspace -> .glyphs""" def _normalize(self, font): # Order the kerning OrderedDict alphabetically # (because the ordering from Glyphs.app is random and that would be @@ -103,7 +104,8 @@ def assertUFORoundtrip(self, font): self._normalize(font) expected = write_to_lines(font) # Don't propagate anchors when intending to round-trip - designspace = to_designspace(font, propagate_anchors=False) + designspace = to_designspace( + font, propagate_anchors=False, minimize_glyphs_diffs=True) # Check that round-tripping in memory is the same as writing on disk roundtrip_in_mem = to_glyphs(designspace) @@ -112,7 +114,7 @@ def assertUFORoundtrip(self, font): directory = tempfile.mkdtemp() path = os.path.join(directory, font.familyName + '.designspace') - designspace.write(path) + write_designspace_and_UFOs(designspace, path) designspace_roundtrip = DesignSpaceDocument( writerClass=InMemoryDocWriter) designspace_roundtrip.read(path) @@ -140,11 +142,12 @@ def write_designspace_and_UFOs(designspace, path): ufo_path = os.path.join(os.path.dirname(path), basename) source.filename = basename source.path = ufo_path - source.font.save(ufo_path) + source.font.save(ufo_path, formatVersion=3) designspace.write(path) class AssertDesignspaceRoundtrip(object): + """Check UFOs + designspace -> .glyphs -> UFOs + designspace""" def assertDesignspacesEqual(self, expected, actual, message=''): directory = tempfile.mkdtemp() @@ -186,7 +189,7 @@ def clean_git_folder(): def assertDesignspaceRoundtrip(self, designspace): directory = tempfile.mkdtemp() - font = to_glyphs(designspace) + font = to_glyphs(designspace, minimize_ufo_diffs=True) # Check that round-tripping in memory is the same as writing on disk roundtrip_in_mem = to_designspace(font) diff --git a/tests/writer_test.py b/tests/writer_test.py index de2899b78..e67f9e9a1 100644 --- a/tests/writer_test.py +++ b/tests/writer_test.py @@ -797,6 +797,7 @@ def test_write_layer(self): name = "{125, 100}"; paths = ( { + closed = 1; } ); userData = { diff --git a/tox.ini b/tox.ini index 420a5ad50..f63452ac7 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py27, py36, htmlcov deps = pytest coverage + ufonormalizer py27: mock>=2.0.0 -rrequirements.txt commands =