diff --git a/.gitignore b/.gitignore index af0f3c117..f0876ad6f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ htmlcov # Autosaved files *~ + +# Files generated by tests +actual* +expected* diff --git a/Lib/glyphsLib/builder/builders.py b/Lib/glyphsLib/builder/builders.py index 7bee6f89b..8e65a78e4 100644 --- a/Lib/glyphsLib/builder/builders.py +++ b/Lib/glyphsLib/builder/builders.py @@ -116,7 +116,6 @@ def masters(self): """ if self._ufos: return self._ufos.values() - kerning_groups = {} # Store set of actually existing master (layer) ids. This helps with # catching dangling layer data that Glyphs may ignore, e.g. when @@ -134,7 +133,6 @@ def masters(self): self.to_ufo_font_attributes(self.family_name) for glyph in self.font.glyphs: - self.to_ufo_glyph_groups(kerning_groups, glyph) glyph_name = glyph.name for layer in glyph.layers.values(): @@ -153,8 +151,10 @@ def masters(self): ufo_glyph = ufo.newGlyph(glyph_name) self.to_ufo_glyph(ufo_glyph, layer, glyph) ufo_layer = ufo.layers.defaultLayer - ufo_layer.lib[GLYPHS_PREFIX + 'layerOrderInGlyph.' + - glyph.name] = self._layer_order_in_glyph(layer) + if self.minimize_glyphs_diffs: + ufo_layer.lib[GLYPHS_PREFIX + 'layerOrderInGlyph.' + + glyph.name] = self._layer_order_in_glyph( + layer) for master_id, glyph_name, layer_name, layer \ in supplementary_layer_data: @@ -181,9 +181,10 @@ def masters(self): else: ufo_layer = ufo_font.layers[layer_name] # TODO: (jany) move as much as possible into layers.py - ufo_layer.lib[GLYPHS_PREFIX + 'layerId'] = layer.layerId - ufo_layer.lib[GLYPHS_PREFIX + 'layerOrderInGlyph.' + - glyph_name] = self._layer_order_in_glyph(layer) + if self.minimize_glyphs_diffs: + ufo_layer.lib[GLYPHS_PREFIX + 'layerId'] = layer.layerId + ufo_layer.lib[GLYPHS_PREFIX + 'layerOrderInGlyph.' + + glyph_name] = self._layer_order_in_glyph(layer) ufo_glyph = ufo_layer.newGlyph(glyph_name) self.to_ufo_glyph(ufo_glyph, layer, layer.parent) @@ -191,12 +192,11 @@ def masters(self): if self.propagate_anchors: 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) + self.to_ufo_groups() + self.to_ufo_kerning() return self._ufos.values() @@ -261,11 +261,11 @@ def instance_data(self): from .features import to_ufo_features from .font import to_ufo_font_attributes from .glyph import to_ufo_glyph, to_ufo_glyph_background + from .groups import to_ufo_groups from .guidelines import to_ufo_guidelines from .hints import to_ufo_hints from .instances import to_designspace_instances - from .kerning import (to_ufo_kerning, to_ufo_glyph_groups, - to_ufo_kerning_groups) + from .kerning import to_ufo_kerning from .masters import to_ufo_master_attributes from .names import to_ufo_names from .paths import to_ufo_paths @@ -348,22 +348,22 @@ def font(self): self.to_glyphs_ordered_masters() self._font = self.glyphs_module.GSFont() + self._ufos = OrderedDict() # Same as in UFOBuilder for index, ufo in enumerate(self.ufos): - kerning_groups = self.to_glyphs_kerning_groups(ufo) - master = self.glyphs_module.GSFontMaster() self.to_glyphs_font_attributes(ufo, master, is_initial=(index == 0)) self.to_glyphs_master_attributes(ufo, master) self._font.masters.insert(len(self._font.masters), master) + self._ufos[master.id] = ufo 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) - self.to_glyphs_kerning(ufo, master) + self.to_glyphs_groups() + self.to_glyphs_kerning() # Now that all GSGlyph are built, restore the glyph order for first_ufo in self.ufos: @@ -437,11 +437,11 @@ def _fake_designspace(self, ufos): from .features import to_glyphs_features from .font import to_glyphs_font_attributes, to_glyphs_ordered_masters from .glyph import to_glyphs_glyph + from .groups import to_glyphs_groups from .guidelines import to_glyphs_guidelines from .hints import to_glyphs_hints from .instances import to_glyphs_instances - from .kerning import (to_glyphs_glyph_groups, to_glyphs_kerning_groups, - to_glyphs_kerning) + from .kerning import to_glyphs_kerning from .layers import to_glyphs_layer, to_glyphs_layer_order from .masters import to_glyphs_master_attributes from .names import to_glyphs_family_names, to_glyphs_master_names diff --git a/Lib/glyphsLib/builder/custom_params.py b/Lib/glyphsLib/builder/custom_params.py index 2f4091a41..26d3c712a 100644 --- a/Lib/glyphsLib/builder/custom_params.py +++ b/Lib/glyphsLib/builder/custom_params.py @@ -271,6 +271,17 @@ def register(handler): for glyphs_name, ufo_name in GLYPHS_UFO_CUSTOM_PARAMS: register(ParamHandler(glyphs_name, ufo_name, glyphs_long_name=ufo_name)) +GLYPHS_UFO_CUSTOM_PARAMS_NO_SHORT_NAME = ( + 'openTypeHheaCaretSlopeRun', + 'openTypeVheaCaretSlopeRun', + 'openTypeHheaCaretSlopeRise', + 'openTypeVheaCaretSlopeRise', + 'openTypeHheaCaretOffset', + 'openTypeVheaCaretOffset', +) +for name in GLYPHS_UFO_CUSTOM_PARAMS_NO_SHORT_NAME: + register(ParamHandler(name, name)) + # convert code page numbers to OS/2 ulCodePageRange bits register(ParamHandler( glyphs_name='codePageRanges', diff --git a/Lib/glyphsLib/builder/glyph.py b/Lib/glyphsLib/builder/glyph.py index 5cb36ce0a..bb63ea208 100644 --- a/Lib/glyphsLib/builder/glyph.py +++ b/Lib/glyphsLib/builder/glyph.py @@ -25,6 +25,7 @@ PUBLIC_PREFIX) SCRIPT_LIB_KEY = GLYPHLIB_PREFIX + 'script' +ORIGINAL_WIDTH_KEY = GLYPHLIB_PREFIX + 'originalWidth' def to_ufo_glyph(self, ufo_glyph, layer, glyph): @@ -70,8 +71,7 @@ def to_ufo_glyph(self, ufo_glyph, layer, glyph): ufo_glyph.font.lib[postscriptNamesKey] = dict() ufo_glyph.font.lib[postscriptNamesKey][ufo_glyph.name] = production_name - for key in ['leftMetricsKey', 'rightMetricsKey', 'widthMetricsKey', - 'leftKerningGroup', 'rightKerningGroup']: + for key in ['leftMetricsKey', 'rightMetricsKey', 'widthMetricsKey']: value = getattr(layer, key, None) if value: ufo_glyph.lib[GLYPHLIB_PREFIX + 'layer.' + key] = value @@ -102,7 +102,8 @@ def to_ufo_glyph(self, ufo_glyph, layer, glyph): elif category == 'Mark' and subCategory == 'Nonspacing' and width > 0: # zero the width of Nonspacing Marks like Glyphs.app does on export # TODO: check for customParameter DisableAllAutomaticBehaviour - ufo_glyph.lib[GLYPHLIB_PREFIX + 'originalWidth'] = width + # FIXME: (jany) also don't do that when rt UFO -> glyphs -> UFO + ufo_glyph.lib[ORIGINAL_WIDTH_KEY] = width ufo_glyph.width = 0 else: ufo_glyph.width = width @@ -176,8 +177,7 @@ def to_glyphs_glyph(self, ufo_glyph, ufo_layer, master): layer = self.to_glyphs_layer(ufo_layer, glyph, master) - for key in ['leftMetricsKey', 'rightMetricsKey', 'widthMetricsKey', - 'leftKerningGroup', 'rightKerningGroup']: + for key in ['leftMetricsKey', 'rightMetricsKey', 'widthMetricsKey']: for prefix, object in (('glyph.', glyph), ('layer.', layer)): full_key = GLYPHLIB_PREFIX + prefix + key if full_key in ufo_glyph.lib: @@ -203,8 +203,8 @@ def to_glyphs_glyph(self, ufo_glyph, ufo_layer, master): layer.width = ufo_glyph.width if category == 'Mark' and sub_category == 'Nonspacing' and layer.width == 0: # Restore originalWidth - if GLYPHLIB_PREFIX + 'originalWidth' in ufo_glyph.lib: - layer.width = ufo_glyph.lib[GLYPHLIB_PREFIX + 'originalWidth'] + if ORIGINAL_WIDTH_KEY in ufo_glyph.lib: + layer.width = ufo_glyph.lib[ORIGINAL_WIDTH_KEY] # TODO: check for customParameter DisableAllAutomaticBehaviour? self.to_glyphs_background_image(ufo_glyph, layer) diff --git a/Lib/glyphsLib/builder/groups.py b/Lib/glyphsLib/builder/groups.py new file mode 100644 index 000000000..a36f3cf3c --- /dev/null +++ b/Lib/glyphsLib/builder/groups.py @@ -0,0 +1,190 @@ +# Copyright 2016 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) + +from collections import defaultdict +import os +import re + +from glyphsLib import classes +from .constants import GLYPHLIB_PREFIX + +UFO_ORIGINAL_KERNING_GROUPS_KEY = GLYPHLIB_PREFIX + 'originalKerningGroups' +UFO_GROUPS_NOT_IN_FEATURE_KEY = GLYPHLIB_PREFIX + 'groupsNotInFeature' +UFO_KERN_GROUP_PATTERN = re.compile('^public\\.kern([12])\\.(.*)$') + + +def to_ufo_groups(self): + # Build groups once and then apply to all UFOs. + groups = defaultdict(list) + + # Classes usually go to the feature file, unless we have our custom flag + group_names = None + if UFO_GROUPS_NOT_IN_FEATURE_KEY in self.font.userData.keys(): + group_names = set(self.font.userData[UFO_GROUPS_NOT_IN_FEATURE_KEY]) + if group_names: + for gsclass in self.font.classes.values(): + if gsclass.name in group_names: + if gsclass.code: + groups[gsclass.name] = gsclass.code.split(' ') + else: + # Empty group: using split like above would produce [''] + groups[gsclass.name] = [] + + # Rebuild kerning groups from `left/rightKerningGroup`s + # Use the original list of kerning groups as a base, to recover + # - the original ordering + # - the kerning groups of glyphs that were not in the font (which can be + # stored in a UFO but not by Glyphs) + recovered = set() + orig_groups = self.font.userData.get(UFO_ORIGINAL_KERNING_GROUPS_KEY) + if orig_groups: + for group, glyphs in orig_groups.items(): + if not glyphs: + # Restore empty group + groups[group] = [] + for glyph_name in glyphs: + # Check that the original value is still valid + match = UFO_KERN_GROUP_PATTERN.match(group) + side = match.group(1) + group_name = match.group(2) + glyph = self.font.glyphs[glyph_name] + if not glyph or getattr( + glyph, _glyph_kerning_attr(glyph, side)) == group_name: + # The original grouping is still valid + groups[group].append(glyph_name) + # Remember not to add this glyph again later + # Thus the original position in the list is preserved + recovered.add((glyph_name, int(side))) + + # Read modified grouping values + for glyph in self.font.glyphs.values(): + for side in 1, 2: + if (glyph.name, side) not in recovered: + attr = _glyph_kerning_attr(glyph, side) + group = getattr(glyph, attr) + if group: + group = 'public.kern%s.%s' % (side, group) + groups[group].append(glyph.name) + + # Update all UFOs with the same info + for ufo in self._ufos.values(): + for name, glyphs in groups.items(): + # Shallow copy to prevent unexpected object sharing + ufo.groups[name] = glyphs[:] + + +def to_glyphs_groups(self): + # Build the GSClasses from the groups of the first UFO. + groups = [] + for ufo in self.ufos: + for name, glyphs in ufo.groups.items(): + if _is_kerning_group(name): + _to_glyphs_kerning_group(self, name, glyphs) + else: + gsclass = classes.GSClass(name, " ".join(glyphs)) + self.font.classes.append(gsclass) + groups.append(name) + if self.minimize_ufo_diffs: + self.font.userData[UFO_GROUPS_NOT_IN_FEATURE_KEY] = groups + break + + # Check that other UFOs are identical and print a warning if not. + for index, ufo in enumerate(self.ufos): + if index == 0: + reference_ufo = ufo + else: + _assert_groups_are_identical(self, reference_ufo, ufo) + + +def _is_kerning_group(name): + return (name.startswith('public.kern1.') or + name.startswith('public.kern2.')) + + +def _to_glyphs_kerning_group(self, name, glyphs): + if self.minimize_ufo_diffs: + # Preserve ordering when going from UFO group + # to left/rightKerningGroup disseminated in GSGlyphs + # back to UFO group. + if not self.font.userData.get(UFO_ORIGINAL_KERNING_GROUPS_KEY): + self.font.userData[UFO_ORIGINAL_KERNING_GROUPS_KEY] = {} + self.font.userData[UFO_ORIGINAL_KERNING_GROUPS_KEY][name] = glyphs + + match = UFO_KERN_GROUP_PATTERN.match(name) + side = match.group(1) + group_name = match.group(2) + for glyph_name in glyphs: + glyph = self.font.glyphs[glyph_name] + if glyph: + setattr(glyph, _glyph_kerning_attr(glyph, side), group_name) + + +def _glyph_kerning_attr(glyph, side): + """Return leftKerningGroup or rightKerningGroup depending on the UFO + group's side (1 or 2) and the glyph's direction (LTR or RTL). + """ + side = int(side) + if _is_ltr(glyph): + if side == 1: + return 'rightKerningGroup' + else: + return 'leftKerningGroup' + else: + # RTL + if side == 1: + return 'leftKerningGroup' + else: + return 'rightKerningGroup' + + +def _is_ltr(glyph): + # TODO: (jany) have a real implem? + # The following one is just to make my simple test pass + if glyph.name.endswith('-hb'): + return False + return True + + +def _assert_groups_are_identical(self, reference_ufo, ufo): + first_time = [True] # Using a mutable as a non-local for closure below + + def _warn(message, *args): + if first_time: + self.logger.warn('Using UFO `%s` as a reference for groups:', + _ufo_logging_ref(reference_ufo)) + first_time.clear() + self.logger.warn(' ' + message, *args) + + # Check for inconsistencies + for group, glyphs in ufo.groups.items(): + if group not in reference_ufo.groups: + _warn("group `%s` from `%s` will be lost because it's not " + "defined in the reference UFO", group, _ufo_logging_ref(ufo)) + reference_glyphs = reference_ufo.groups[group] + if glyphs != reference_glyphs: + _warn("group `%s` from `%s` will not be stored accurately because " + "it is different from the reference UFO", group, + _ufo_logging_ref(ufo)) + _warn(" reference = %s", ' '.join(glyphs)) + _warn(" current = %s", ' '.join(reference_glyphs)) + + +def _ufo_logging_ref(ufo): + """Return a string that can identify this UFO in logs.""" + if ufo.path: + return os.path.basename(ufo.path) + return ufo.info.styleName diff --git a/Lib/glyphsLib/builder/kerning.py b/Lib/glyphsLib/builder/kerning.py index e9a21084a..afb4f2fe9 100644 --- a/Lib/glyphsLib/builder/kerning.py +++ b/Lib/glyphsLib/builder/kerning.py @@ -15,20 +15,17 @@ from __future__ import (print_function, division, absolute_import, unicode_literals) -import logging import re -from collections import defaultdict -logger = logging.getLogger(__name__) +UFO_KERN_GROUP_PATTERN = re.compile('^public\\.kern([12])\\.(.*)$') -GROUP_KEYS = { - '1': 'rightKerningGroup', - '2': 'leftKerningGroup'} -UFO_KERN_GROUP_PATTERN = re.compile('^public\\.kern([12])\\.(.*)$') +def to_ufo_kerning(self): + for master_id, kerning in self.font.kerning.items(): + _to_ufo_kerning(self, self._ufos[master_id], kerning) -def to_ufo_kerning(self, ufo, kerning_data): +def _to_ufo_kerning(self, ufo, kerning_data): """Add .glyphs kerning to an UFO.""" warning_msg = 'Non-existent glyph class %s found in kerning rules.' @@ -40,7 +37,7 @@ def to_ufo_kerning(self, ufo, kerning_data): if left_is_class: left = 'public.kern1.%s' % match.group(1) if left not in ufo.groups: - # logger.warn(warning_msg % left) + # self.logger.warn(warning_msg % left) pass for right, kerning_val in pairs.items(): match = re.match(r'@MMK_R_(.+)', right) @@ -48,7 +45,7 @@ def to_ufo_kerning(self, ufo, kerning_data): if right_is_class: right = 'public.kern2.%s' % match.group(1) if right not in ufo.groups: - # logger.warn(warning_msg % right) + # self.logger.warn(warning_msg % right) pass if left_is_class != right_is_class: if left_is_class: @@ -60,23 +57,11 @@ def to_ufo_kerning(self, ufo, kerning_data): seen = {} for classname, glyph, is_left_class in reversed(class_glyph_pairs): - _remove_rule_if_conflict(ufo, seen, classname, glyph, is_left_class) - - -def to_glyphs_kerning(self, ufo, master): - """Add UFO kerning to GSFontMaster.""" - for (left, right), value in ufo.kerning.items(): - left_match = UFO_KERN_GROUP_PATTERN.match(left) - right_match = UFO_KERN_GROUP_PATTERN.match(right) - if left_match: - left = '@MMK_L_{}'.format(left_match.group(2)) - if right_match: - right = '@MMK_R_{}'.format(right_match.group(2)) - self.font.setKerningForPair(master.id, left, right, value) - # FIXME: (jany) handle conflicts? + _remove_rule_if_conflict(self, ufo, seen, classname, glyph, + is_left_class) -def _remove_rule_if_conflict(ufo, seen, classname, glyph, is_left_class): +def _remove_rule_if_conflict(self, ufo, seen, classname, glyph, is_left_class): """Check if a class-to-glyph kerning rule has a conflict with any existing rule in `seen`, and remove any conflicts if they exist. """ @@ -97,7 +82,7 @@ def _remove_rule_if_conflict(ufo, seen, classname, glyph, is_left_class): if (existing_rule is not None and existing_rule[-1] != val and pair not in ufo.kerning): - logger.warn( + self.logger.warn( 'Conflicting kerning rules found in %s master for glyph pair ' '"%s, %s" (%s and %s), removing pair from latter rule' % ((ufo.info.styleName,) + pair + (existing_rule, rule))) @@ -112,45 +97,15 @@ def _remove_rule_if_conflict(ufo, seen, classname, glyph, is_left_class): ufo.kerning[pair] = val -def to_ufo_glyph_groups(self, kerning_groups, glyph_data): - """Add a glyph to its kerning groups, creating new groups if necessary.""" - - glyph_name = glyph_data.name - for side, group_key in GROUP_KEYS.items(): - group = getattr(glyph_data, group_key) - if group is None or len(group) == 0: - continue - group = 'public.kern%s.%s' % (side, group) - kerning_groups[group] = kerning_groups.get(group, []) + [glyph_name] - - -def to_glyphs_glyph_groups(self, kerning_groups, glyph): - """Write kerning groups to the GSGlyph. - Uses the ouput of to_glyphs_kerning_groups. - """ - for group_key, group_name in kerning_groups.items(): - setattr(glyph, group_key, group_name) - - -def to_ufo_kerning_groups(self, ufo, kerning_groups): - """Add kerning groups to an UFO.""" - - for name, glyphs in kerning_groups.items(): - ufo.groups[name] = glyphs - - -def to_glyphs_kerning_groups(self, ufo): - """Extract all kerning group information from UFO. - Return a dict {glyph name: dict {rightKerningGroup: leftKerningGroup: }} - """ - result = defaultdict(dict) - for group, members in ufo.groups.items(): - match = UFO_KERN_GROUP_PATTERN.match(group) - if not match: - continue - side = match.group(1) - group_name = match.group(2) - for glyph_name in members: - result[glyph_name][GROUP_KEYS[side]] = group_name - - return result +def to_glyphs_kerning(self): + """Add UFO kerning to GSFont.""" + for master_id, ufo in self._ufos.items(): + for (left, right), value in ufo.kerning.items(): + left_match = UFO_KERN_GROUP_PATTERN.match(left) + right_match = UFO_KERN_GROUP_PATTERN.match(right) + if left_match: + left = '@MMK_L_{}'.format(left_match.group(2)) + if right_match: + right = '@MMK_R_{}'.format(right_match.group(2)) + self.font.setKerningForPair(master_id, left, right, value) + # FIXME: (jany) handle conflicts? diff --git a/tests/builder_test.py b/tests/builder/builder_test.py similarity index 100% rename from tests/builder_test.py rename to tests/builder/builder_test.py diff --git a/tests/lib_and_user_data.png b/tests/builder/lib_and_user_data.png similarity index 100% rename from tests/lib_and_user_data.png rename to tests/builder/lib_and_user_data.png diff --git a/tests/lib_and_user_data.uml b/tests/builder/lib_and_user_data.uml similarity index 100% rename from tests/lib_and_user_data.uml rename to tests/builder/lib_and_user_data.uml diff --git a/tests/lib_and_user_data_test.py b/tests/builder/lib_and_user_data_test.py similarity index 87% rename from tests/lib_and_user_data_test.py rename to tests/builder/lib_and_user_data_test.py index f6ae70c4a..d8972d62d 100644 --- a/tests/lib_and_user_data_test.py +++ b/tests/builder/lib_and_user_data_test.py @@ -18,6 +18,7 @@ import base64 import os import pytest +from collections import OrderedDict import defcon from glyphsLib import classes @@ -243,3 +244,36 @@ def test_node_user_data_into_glif_lib(): path = font.glyphs['a'].layers['M1'].paths[0] assert path.nodes[1].userData['nodeUserDataKey1'] == 'nodeUserDataValue1' assert path.nodes[4].userData['nodeUserDataKey2'] == 'nodeUserDataValue2' + + +def test_lib_data_types(): + # Test the roundtrip of a few basic types both at the top level and in a + # nested object. + data = OrderedDict({ + 'boolean': True, + 'smooth': False, + 'integer': 1, + 'float': 0.5, + 'array': [], + 'dict': {}, + }) + ufo = defcon.Font() + a = ufo.newGlyph('a') + for key, value in data.items(): + a.lib[key] = value + a.lib['nestedDict'] = dict(data) + a.lib['nestedArray'] = list(data.values()) + a.lib['crazyNesting'] = [{'a': [{'b': [dict(data)]}]}] + + font = to_glyphs([ufo]) + ufo, = to_ufos(font) + + for index, (key, value) in enumerate(data.items()): + assert value == ufo['a'].lib[key] + assert value == ufo['a'].lib['nestedDict'][key] + assert value == ufo['a'].lib['nestedArray'][index] + assert value == ufo['a'].lib['crazyNesting'][0]['a'][0]['b'][0][key] + assert type(value) == type(ufo['a'].lib[key]) + assert type(value) == type(ufo['a'].lib['nestedDict'][key]) + assert type(value) == type(ufo['a'].lib['nestedArray'][index]) + assert type(value) == type(ufo['a'].lib['crazyNesting'][0]['a'][0]['b'][0][key]) diff --git a/tests/roundtrip_test.py b/tests/builder/roundtrip_test.py similarity index 94% rename from tests/roundtrip_test.py rename to tests/builder/roundtrip_test.py index 658e09db6..28d5f54fa 100644 --- a/tests/roundtrip_test.py +++ b/tests/builder/roundtrip_test.py @@ -31,7 +31,7 @@ def test_empty_font(self): def test_GlyphsUnitTestSans(self): filename = os.path.join(os.path.dirname(__file__), - 'data/GlyphsUnitTestSans.glyphs') + '../data/GlyphsUnitTestSans.glyphs') with open(filename) as f: font = glyphsLib.load(f) self.assertUFORoundtrip(font) diff --git a/tests/builder/to_glyphs_test.py b/tests/builder/to_glyphs_test.py new file mode 100644 index 000000000..ac05c8b21 --- /dev/null +++ b/tests/builder/to_glyphs_test.py @@ -0,0 +1,179 @@ +# coding=UTF-8 +# +# Copyright 2016 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 pytest + +import defcon + +from glyphsLib import to_glyphs, to_ufos + +# TODO: (jany) think hard about the ordering and RTL/LTR +# TODO: (jany) make one generic test with data using pytest + + +@pytest.mark.skip +def test_anchors_with_same_name_correct_order_rtl(): + ufo = defcon.Font() + g = ufo.newGlyph('laam_alif') + # Append the anchors in the correct order + g.appendAnchor(dict(x=50, y=600, name='top')) + g.appendAnchor(dict(x=250, y=600, name='top')) + + font = to_glyphs([ufo]) + + top1, top2 = font.glyphs['laam_alif'].layers[0].anchors + + assert top1.name == 'top_1' + assert top1.x == 50 + assert top1.y == 600 + assert top2.name == 'top_2' + assert top2.x == 250 + assert top2.y == 600 + + +@pytest.mark.skip +def test_anchors_with_same_name_wrong_order_rtl(): + ufo = defcon.Font() + g = ufo.newGlyph('laam_alif') + # Append the anchors in the wrong order + g.appendAnchor(dict(x=250, y=600, name='top')) + g.appendAnchor(dict(x=50, y=600, name='top')) + + font = to_glyphs([ufo]) + + top1, top2 = font.glyphs['laam_alif'].layers[0].anchors + + # FIXME: (jany) think hard about the ordering and LTR + assert top1.name == 'top_1' + assert top1.x == 50 + assert top1.y == 600 + assert top2.name == 'top_2' + assert top2.x == 250 + assert top2.y == 600 + + +@pytest.mark.skip +def test_anchors_with_same_name_correct_order_ltr(): + ufo = defcon.Font() + g = ufo.newGlyph('laam_alif') + # Append the anchors in the correct order + g.appendAnchor(dict(x=50, y=600, name='top')) + g.appendAnchor(dict(x=250, y=600, name='top')) + + font = to_glyphs([ufo]) + + top1, top2 = font.glyphs['laam_alif'].layers[0].anchors + + # FIXME: (jany) think hard about the ordering and RTL/LTR + assert top1.name == 'top_1' + assert top1.x == 50 + assert top1.y == 600 + assert top2.name == 'top_2' + assert top2.x == 250 + assert top2.y == 600 + + +@pytest.mark.skip +def test_anchors_with_same_name_wrong_order_ltr(): + ufo = defcon.Font() + g = ufo.newGlyph('laam_alif') + # Append the anchors in the wrong order + g.appendAnchor(dict(x=250, y=600, name='top')) + g.appendAnchor(dict(x=50, y=600, name='top')) + + font = to_glyphs([ufo]) + + top1, top2 = font.glyphs['laam_alif'].layers[0].anchors + + # FIXME: (jany) think hard about the ordering and LTR + assert top1.name == 'top_1' + assert top1.x == 50 + assert top1.y == 600 + assert top2.name == 'top_2' + assert top2.x == 250 + assert top2.y == 600 + + +def test_groups(): + ufo = defcon.Font() + ufo.newGlyph('T') + ufo.newGlyph('e') + ufo.newGlyph('o') + samekh = ufo.newGlyph('samekh-hb') + samekh.unicode = 0x05E1 + resh = ufo.newGlyph('resh-hb') + resh.unicode = 0x05E8 + ufo.groups['public.kern1.T'] = ['T'] + ufo.groups['public.kern2.oe'] = ['o', 'e'] + ufo.groups['com.whatever.Te'] = ['T', 'e'] + # Groups can contain glyphs that are not in the font and that should + # be preserved as well + ufo.groups['public.kern1.notInFont'] = ['L'] + ufo.groups['public.kern1.halfInFont'] = ['o', 'b', 'p'] + ufo.groups['com.whatever.notInFont'] = ['i', 'j'] + # Empty groups as well (found in the wild) + ufo.groups['public.kern1.empty'] = [] + ufo.groups['com.whatever.empty'] = [] + # Groups for RTL glyphs. In a UFO RTL kerning pair, kern1 is for the glyph + # on the left visually (the first that gets written when writing RTL) + # The example below with Resh and Samekh comes from: + # https://forum.glyphsapp.com/t/dramatic-bug-in-hebrew-kerning/4093 + ufo.groups['public.kern1.hebrewLikeT'] = ['resh-hb'] + ufo.groups['public.kern2.hebrewLikeO'] = ['samekh-hb'] + groups_dict = dict(ufo.groups) + + # TODO: add a test with 2 UFOs with conflicting data + # TODO: add a test with with both UFO groups and feature file classes + # TODO: add a test with UFO groups that conflict with feature file classes + font = to_glyphs([ufo], minimize_ufo_diffs=True) + + # Kerning for existing glyphs is stored in GSGlyph.left/rightKerningGroup + assert font.glyphs['T'].rightKerningGroup == 'T' + assert font.glyphs['o'].leftKerningGroup == 'oe' + assert font.glyphs['e'].leftKerningGroup == 'oe' + # In Glyphs, rightKerningGroup and leftKerningGroup refer to the sides of + # the glyph, they don't swap for RTL glyphs + assert font.glyphs['resh-hb'].leftKerningGroup == 'hebrewLikeT' + assert font.glyphs['samekh-hb'].rightKerningGroup == 'hebrewLikeO' + + # Non-kerning groups are stored as classes + assert font.classes['com.whatever.Te'].code == 'T e' + assert font.classes['com.whatever.notInFont'].code == 'i j' + # Kerning groups with some characters not in the font are also saved + # somehow, but we don't care how, that fact will be better tested by the + # roundtrip test a few lines below + + ufo, = to_ufos(font) + + # Check that nothing has changed + assert dict(ufo.groups) == groups_dict + + # Check that changing the `left/rightKerningGroup` fields in Glyphs + # updates the UFO kerning groups + font.glyphs['T'].rightKerningGroup = 'newNameT' + font.glyphs['o'].rightKerningGroup = 'onItsOwnO' + + del groups_dict['public.kern1.T'] + groups_dict['public.kern1.newNameT'] = ['T'] + groups_dict['public.kern1.halfInFont'].remove('o') + groups_dict['public.kern1.onItsOwnO'] = ['o'] + + ufo, = to_ufos(font) + + assert dict(ufo.groups) == groups_dict diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 1638c95e2..bb7b7d6e9 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -192,15 +192,21 @@ def assertDesignspaceRoundtrip(self, 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) + roundtrip_in_mem = to_designspace(font, propagate_anchors=False) tmpfont_path = os.path.join(directory, 'font.glyphs') font.save(tmpfont_path) font_rt = classes.GSFont(tmpfont_path) - roundtrip = to_designspace(font_rt) + roundtrip = to_designspace(font_rt, propagate_anchors=False) font.save('intermediary.glyphs') + write_designspace_and_UFOs(designspace, 'expected/test.designspace') + for source in designspace.sources: + normalizeUFO(source.path, floatPrecision=3, writeModTimes=False) + write_designspace_and_UFOs(roundtrip, 'actual/test.designspace') + for source in roundtrip.sources: + normalizeUFO(source.path, floatPrecision=3, writeModTimes=False) # self.assertDesignspacesEqual( # roundtrip_in_mem, roundtrip, # "The round-trip in memory or written to disk should be equivalent")