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 000000000..7e7b40e90 Binary files /dev/null and b/tests/lib_and_user_data.png differ 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 =