diff --git a/Lib/glyphsLib/__init__.py b/Lib/glyphsLib/__init__.py index 24bbc1878..d0d4ef1bb 100644 --- a/Lib/glyphsLib/__init__.py +++ b/Lib/glyphsLib/__init__.py @@ -77,7 +77,8 @@ def build_masters(filename, master_dir, designspace_instance_dir=None, font = GSFont(filename) designspace = to_designspace( - font, family_name=family_name, propagate_anchors=propagate_anchors) + font, family_name=family_name, propagate_anchors=propagate_anchors, + instance_dir=designspace_instance_dir) ufos = [] for source in designspace.sources: ufos.append(source.font) @@ -88,9 +89,9 @@ def build_masters(filename, master_dir, designspace_instance_dir=None, if designspace_instance_dir is not None: designspace_path = os.path.join(master_dir, designspace.filename) designspace.write(designspace_path) - # All the instance data should be in the designspace - instance_data = designspace.instances - return ufos, designspace_path, instance_data + # All the instance data should be in the designspace. That's why for + # now we return the designspace in place of `instance_data`. + return ufos, designspace_path, designspace else: return ufos diff --git a/Lib/glyphsLib/builder/axes.py b/Lib/glyphsLib/builder/axes.py index 991a76ee2..c3bed9aec 100644 --- a/Lib/glyphsLib/builder/axes.py +++ b/Lib/glyphsLib/builder/axes.py @@ -25,41 +25,20 @@ # This is a key into GSFont.userData to store axes defined in the designspace AXES_KEY = GLYPHLIB_PREFIX + 'axes' -# From the spec: https://www.microsoft.com/typography/otspec/os2.htm#wtc -CLASSES_DICT = { - 'wght': { - 100: ('Thin', 100), - 200: ('Extra-light', 200), - 300: ('Light', 300), - 400: ('Regular', 400), - 500: ('Medium', 500), - 600: ('Semi-bold', 600), - 700: ('Bold', 700), - 800: ('Extra-bold', 800), - 900: ('Black', 900), - }, - 'wdth': { - 1: ('Ultra-condensed', 50), - 2: ('Extra-condensed', 62.5), - 3: ('Condensed', 75), - 4: ('Semi-condensed', 87.5), - 5: ('Medium', 100), - 6: ('Semi-expanded', 112.5), - 7: ('Expanded', 125), - 8: ('Extra-expanded', 150), - 9: ('Ultra-expanded', 200), - } +# From the spec: https://docs.microsoft.com/en-gb/typography/opentype/spec/os2#uswidthclass +WIDTH_CLASS_TO_VALUE = { + 1: 50, # Ultra-condensed + 2: 62.5, # Extra-condensed + 3: 75, # Condensed + 4: 87.5, # Semi-condensed + 5: 100, # Medium + 6: 112.5, # Semi-expanded + 7: 125, # Expanded + 8: 150, # Extra-expanded + 9: 200, # Ultra-expanded } -def class_to_name(axis, ufo_class): - """ - >>> class_to_name('wdth', 7) - 'Expanded' - """ - return CLASSES_DICT[axis][int(ufo_class)][0] - - def class_to_value(axis, ufo_class): """ >>> class_to_value('wdth', 7) @@ -68,40 +47,76 @@ def class_to_value(axis, ufo_class): if axis == 'wght': # 600.0 => 600, 250 => 250 return int(ufo_class) - return CLASSES_DICT[axis][int(ufo_class)][1] + elif axis == 'wdth': + return WIDTH_CLASS_TO_VALUE[int(ufo_class)] + + raise NotImplementedError + +def _nospace_lookup(dict, key): + try: + return dict[key] + except KeyError: + # Even though the Glyphs UI strings are supposed to be fixed, + # some Noto files contain variants of them that have spaces. + key = ''.join(str(key).split()) + return dict[key] -def user_loc_code_to_value(axis_tag, user_loc): - """ Go from Glyphs UI strings to user space location. - >>> user_loc_code_to_value('wght', 'ExtraLight') +def user_loc_string_to_value(axis_tag, user_loc): + """Go from Glyphs UI strings to user space location. + Returns None if the string is invalid. + + >>> user_loc_string_to_value('wght', 'ExtraLight') 250 - >>> user_loc_code_to_value('wdth', 'SemiCondensed') + >>> user_loc_string_to_value('wdth', 'SemiCondensed') 87.5 + >>> user_loc_string_to_value('wdth', 'Clearly Not From Glyphs UI') + None """ if axis_tag == 'wght': - return class_to_value('wght', WEIGHT_CODES.get(user_loc, user_loc)) - if axis_tag == 'wdth': - return class_to_value('wdth', WIDTH_CODES.get(user_loc, user_loc)) + try: + value = _nospace_lookup(WEIGHT_CODES, user_loc) + except KeyError: + return None + return class_to_value('wght', value) + elif axis_tag == 'wdth': + try: + value = _nospace_lookup(WIDTH_CODES, user_loc) + except KeyError: + return None + return class_to_value('wdth', value) # Currently this function should only be called with a width or weight raise NotImplementedError def user_loc_value_to_class(axis_tag, user_loc): - """ + """Return the OS/2 weight or width class that is closest to the provided + user location. For weight the user location is between 0 and 1000 and for + width it is a percentage. + + >>> user_loc_value_to_class('wght', 310) + 300 >>> user_loc_value_to_class('wdth', 62) 2 """ if axis_tag == 'wght': return int(user_loc) - return min(sorted(CLASSES_DICT[axis_tag].items()), - key=lambda item: abs(item[1][1] - user_loc))[0] + elif axis_tag == 'wdth': + return min(sorted(WIDTH_CLASS_TO_VALUE.items()), + key=lambda item: abs(item[1] - user_loc))[0] + raise NotImplementedError -def user_loc_value_to_code(axis_tag, user_loc): - """ - >>> user_loc_value_to_code('wdth', 150) + +def user_loc_value_to_instance_string(axis_tag, user_loc): + """Return the Glyphs UI string (from the instance dropdown) that is + closest to the provided user location. + + >>> user_loc_value_to_string('wght', 430) + 'Regular' + >>> user_loc_value_to_string('wdth', 150) 'Extra Expanded' """ codes = {} @@ -128,9 +143,6 @@ def to_designspace_axes(self): axis.tag = axis_def.tag axis.name = axis_def.name - regularDesignLoc = axis_def.get_design_loc(regular_master) - regularUserLoc = axis_def.get_user_loc(regular_master) - axis.labelNames = {"en": axis_def.name} instance_mapping = [] for instance in self.font.instances: @@ -138,24 +150,26 @@ def to_designspace_axes(self): designLoc = axis_def.get_design_loc(instance) userLoc = axis_def.get_user_loc(instance) instance_mapping.append((userLoc, designLoc)) - - # FIXME: (jany) why the next two lines? - if designLoc == regularDesignLoc: - regularUserLoc = userLoc instance_mapping = sorted(set(instance_mapping)) # avoid duplicates master_mapping = [] for master in self.font.masters: designLoc = axis_def.get_design_loc(master) - # FIXME: (jany) in latest Glyphs (1113) masters don't have - # a user loc - userLoc = axis_def.get_user_loc(master) + # Glyphs masters don't have a user location + userLoc = designLoc master_mapping.append((userLoc, designLoc)) master_mapping = sorted(set(master_mapping)) - minimum = maximum = default = axis_def.default_user_loc # Prefer the instance-based mapping mapping = instance_mapping or master_mapping + + regularDesignLoc = axis_def.get_design_loc(regular_master) + # Glyphs masters don't have a user location, so we compute it by + # looking at the axis mapping in reverse. + reverse_mapping = [(dl, ul) for ul, dl in mapping] + regularUserLoc = interp(reverse_mapping, regularDesignLoc) + + minimum = maximum = default = axis_def.default_user_loc if mapping: minimum = min([userLoc for userLoc, _ in mapping]) maximum = max([userLoc for userLoc, _ in mapping]) @@ -233,6 +247,10 @@ def to_glyphs_axes(self): class AxisDefinition(object): + """Centralize the code that deals with axis locations, user location versus + design location, associated OS/2 table codes, etc. + """ + def __init__(self, tag, name, design_loc_key, default_design_loc=0.0, user_loc_key=None, user_loc_param=None, default_user_loc=0.0): self.tag = tag @@ -243,65 +261,93 @@ def __init__(self, tag, name, design_loc_key, default_design_loc=0.0, self.user_loc_param = user_loc_param self.default_user_loc = default_user_loc - def get_design_loc(self, master_or_instance): - return getattr(master_or_instance, self.design_loc_key) + def get_design_loc(self, glyphs_master_or_instance): + """Get the design location (aka interpolation value) of a Glyphs + master or instance along this axis. For example for the weight + axis it could be the thickness of a stem, for the width a percentage + of extension with respect to the normal width. + """ + return getattr(glyphs_master_or_instance, self.design_loc_key) def set_design_loc(self, master_or_instance, value): + """Set the design location of a Glyphs master or instance.""" setattr(master_or_instance, self.design_loc_key, value) - def get_user_loc(self, master_or_instance): + def get_user_loc(self, instance): + """Get the user location of a Glyphs instance. + Masters in Glyphs don't have a user location. + The user location is what the user sees on the slider in his + variable-font-enabled UI. For weight it is a value between 0 and 1000, + 400 being Regular and 700 Bold. + For width... FIXME: clarify what it is for the width. + """ + assert isinstance(instance, classes.GSInstance) if self.tag == 'wdth': # FIXME: (jany) existing test "DesignspaceTestTwoAxes.designspace" # suggests that the user location is the same as the design loc # for the width only - return self.get_design_loc(master_or_instance) + return self.get_design_loc(instance) user_loc = self.default_user_loc if self.user_loc_key is not None: - user_loc = getattr(master_or_instance, self.user_loc_key) - user_loc = user_loc_code_to_value(self.tag, user_loc) + # Only weight and with have a custom user location. + # The `user_loc_key` gives a "location code" = Glyphs UI string + user_loc = getattr(instance, self.user_loc_key) + user_loc = user_loc_string_to_value(self.tag, user_loc) + if user_loc is None: + user_loc = self.default_user_loc # The custom param takes over the key if it exists # e.g. for weight: # key = "weight" -> "Bold" -> 700 # but param = "weightClass" -> 600 => 600 wins if self.user_loc_param is not None: - class_ = master_or_instance.customParameters[self.user_loc_param] + class_ = instance.customParameters[self.user_loc_param] if class_ is not None: user_loc = class_to_value(self.tag, class_) return user_loc - def set_user_loc(self, master_or_instance, value): + def set_user_loc(self, instance, value): + """Set the user location of a Glyphs instance.""" + assert isinstance(instance, classes.GSInstance) # Try to set the key if possible, i.e. if there is a key, and # if there exists a code that can represent the given value, e.g. # for "weight": 600 can be represented by SemiBold so we use that, # but for 550 there is no code so we will have to set the custom # parameter as well. - code = user_loc_value_to_code(self.tag, value) - value_for_code = user_loc_code_to_value(self.tag, code) + code = user_loc_value_to_instance_string(self.tag, value) + value_for_code = user_loc_string_to_value(self.tag, code) if self.user_loc_key is not None: - setattr(master_or_instance, self.user_loc_key, code) + setattr(instance, self.user_loc_key, code) if self.user_loc_param is not None and value != value_for_code: try: class_ = user_loc_value_to_class(self.tag, value) - master_or_instance.customParameters[self.user_loc_param] = class_ + instance.customParameters[self.user_loc_param] = class_ except: pass - def set_user_loc_code(self, master_or_instance, code): + def set_user_loc_code(self, instance, code): + assert isinstance(instance, classes.GSInstance) # The previous method `set_user_loc` will not roundtrip every # time, for example for value = 600, both "DemiBold" and "SemiBold" # would work, so we provide this other method to set a specific code. if self.user_loc_key is not None: - setattr(master_or_instance, self.user_loc_key, code) - - -DEFAULT_AXES_DEFS = ( - AxisDefinition('wght', 'Weight', 'weightValue', 100.0, - 'weight', 'weightClass', 400.0), - AxisDefinition('wdth', 'Width', 'widthValue', 100.0, - 'width', 'widthClass', 100.0), - AxisDefinition('XXXX', 'Custom', 'customValue', 0.0, None, None, 0.0), -) + setattr(instance, self.user_loc_key, code) + + def set_ufo_user_loc(self, ufo, value): + if self.name not in ('Weight', 'Width'): + raise NotImplementedError + class_ = user_loc_value_to_class(self.tag, value) + ufo_key = "".join(['openTypeOS2', self.name, 'Class']) + setattr(ufo.info, ufo_key, class_) + + +WEIGHT_AXIS_DEF = AxisDefinition('wght', 'Weight', 'weightValue', 100.0, + 'weight', 'weightClass', 400.0) +WIDTH_AXIS_DEF = AxisDefinition('wdth', 'Width', 'widthValue', 100.0, + 'width', 'widthClass', 100.0) +CUSTOM_AXIS_DEF = AxisDefinition('XXXX', 'Custom', 'customValue', 0.0, + None, None, 0.0) +DEFAULT_AXES_DEFS = (WEIGHT_AXIS_DEF, WIDTH_AXIS_DEF, CUSTOM_AXIS_DEF) # Adapted from PR https://github.com/googlei18n/glyphsLib/pull/306 diff --git a/Lib/glyphsLib/builder/builders.py b/Lib/glyphsLib/builder/builders.py index 85040b7dd..f8853baf6 100644 --- a/Lib/glyphsLib/builder/builders.py +++ b/Lib/glyphsLib/builder/builders.py @@ -27,7 +27,8 @@ from glyphsLib import classes, glyphdata_generated from .constants import PUBLIC_PREFIX, GLYPHS_PREFIX, FONT_CUSTOM_PARAM_PREFIX -from .axes import DEFAULT_AXES_DEFS, find_base_style, class_to_value +from .axes import (WEIGHT_AXIS_DEF, WIDTH_AXIS_DEF, find_base_style, + class_to_value) GLYPH_ORDER_KEY = PUBLIC_PREFIX + 'glyphOrder' @@ -212,7 +213,7 @@ def _layer_order_in_glyph(self, layer): # TODO: move to layers.py # TODO: optimize? for order, glyph_layer in enumerate(layer.parent.layers.values()): - if glyph_layer == layer: + if glyph_layer is layer: return order return None @@ -422,8 +423,8 @@ def _fake_designspace(self, ufos): # Make weight and width axis if relevant for info_key, axis_def in zip( - ('openTypeOS2WeightClass', 'openTypeOS2WidthClass'), - DEFAULT_AXES_DEFS): + ('openTypeOS2WeightClass', 'openTypeOS2WidthClass'), + (WEIGHT_AXIS_DEF, WIDTH_AXIS_DEF)): axis = designspace.newAxisDescriptor() axis.tag = axis_def.tag axis.name = axis_def.name diff --git a/Lib/glyphsLib/builder/custom_params.py b/Lib/glyphsLib/builder/custom_params.py index 4161b4c39..f8c828393 100644 --- a/Lib/glyphsLib/builder/custom_params.py +++ b/Lib/glyphsLib/builder/custom_params.py @@ -25,6 +25,7 @@ CODEPAGE_RANGES, REVERSE_CODEPAGE_RANGES) from .features import replace_feature +# TODO: update this documentation """Set Glyphs custom parameters in UFO info or lib, where appropriate. Custom parameter data can be pre-parsed out of Glyphs data and provided via diff --git a/Lib/glyphsLib/builder/instances.py b/Lib/glyphsLib/builder/instances.py index 489f97fed..057c00fd9 100644 --- a/Lib/glyphsLib/builder/instances.py +++ b/Lib/glyphsLib/builder/instances.py @@ -19,12 +19,13 @@ import os from glyphsLib.util import build_ufo_path -from glyphsLib.classes import WEIGHT_CODES +from glyphsLib.classes import WEIGHT_CODES, GSCustomParameter from .constants import (GLYPHS_PREFIX, GLYPHLIB_PREFIX, FONT_CUSTOM_PARAM_PREFIX, MASTER_CUSTOM_PARAM_PREFIX) from .names import build_stylemap_names from .masters import UFO_FILENAME_KEY -from .axes import get_axis_definitions, is_instance_active, interp +from .axes import (get_axis_definitions, is_instance_active, interp, + WEIGHT_AXIS_DEF, WIDTH_AXIS_DEF) EXPORT_KEY = GLYPHS_PREFIX + 'export' WIDTH_KEY = GLYPHS_PREFIX + 'width' @@ -32,6 +33,7 @@ FULL_FILENAME_KEY = GLYPHLIB_PREFIX + 'fullFilename' MANUAL_INTERPOLATION_KEY = GLYPHS_PREFIX + 'manualInterpolation' INSTANCE_INTERPOLATIONS_KEY = GLYPHS_PREFIX + 'intanceInterpolations' +CUSTOM_PARAMETERS_KEY = GLYPHS_PREFIX + 'customParameters' def to_designspace_instances(self): @@ -76,9 +78,12 @@ def _to_designspace_instance(self, instance): ufo_instance.filename = build_ufo_path( instance_dir, ufo_instance.familyName, ufo_instance.styleName) + designspace_axis_tags = set(a.tag for a in self.designspace.axes) location = {} for axis_def in get_axis_definitions(self.font): - location[axis_def.name] = axis_def.get_design_loc(instance) + # Only write locations along defined axes + if axis_def.tag in designspace_axis_tags: + location[axis_def.name] = axis_def.get_design_loc(instance) ufo_instance.location = location # FIXME: (jany) should be the responsibility of ufo2ft? @@ -106,7 +111,27 @@ def _to_designspace_instance(self, instance): ufo_instance.lib[INSTANCE_INTERPOLATIONS_KEY] = instance.instanceInterpolations ufo_instance.lib[MANUAL_INTERPOLATION_KEY] = instance.manualInterpolation - # TODO: put the userData/customParameters in lib + # Strategy: dump all custom parameters into the InstanceDescriptor. + # Later, when using `glyphsLib.interpolation.apply_instance_data`, + # we will dig out those custom parameters using + # `InstanceDescriptorAsGSInstance` and apply them to the instance UFO + # with `to_ufo_custom_params`. + # NOTE: customParameters are not a dict! One key can have several values + params = [] + for p in instance.customParameters: + if p.name in ('familyName', 'postscriptFontName', 'fileName', + FULL_FILENAME_KEY): + # These will be stored in the official descriptor attributes + continue + if p.name in ('weightClass', 'widthClass'): + # No need to store these ones because we can recover them by + # reading the mapping backward, because the mapping is built from + # where the instances are. + continue + params.append((p.name, p.value)) + if params: + ufo_instance.lib[CUSTOM_PARAMETERS_KEY] = params + self.designspace.addInstance(ufo_instance) @@ -232,9 +257,96 @@ def to_glyphs_instances(self): # if instance.manualInterpolation: warn about data loss pass + if CUSTOM_PARAMETERS_KEY in ufo_instance.lib: + for name, value in ufo_instance.lib[CUSTOM_PARAMETERS_KEY]: + instance.customParameters.append( + GSCustomParameter(name, value)) + if self.minimize_ufo_diffs: instance.customParameters[ FULL_FILENAME_KEY] = ufo_instance.filename # FIXME: (jany) cannot `.append()` because no proxy => no parent self.font.instances = self.font.instances + [instance] + + +class InstanceDescriptorAsGSInstance(object): + """Wraps a designspace InstanceDescriptor and makes it behave like a + GSInstance, just enough to use the descriptor as a source of custom + parameters for `to_ufo_custom_parameters` + """ + def __init__(self, descriptor): + self._descriptor = descriptor + + # Having a simple list is enough because `to_ufo_custom_params` does + # not use the fake dictionary interface. + self.customParameters = [] + if CUSTOM_PARAMETERS_KEY in descriptor.lib: + for name, value in descriptor.lib[CUSTOM_PARAMETERS_KEY]: + self.customParameters.append(GSCustomParameter(name, value)) + + +def _set_class_from_instance(ufo, designspace, instance, axis_def): + # FIXME: (jany) copy-pasted from above, factor into method? + design_loc = None + try: + design_loc = instance.location[axis_def.name] + except KeyError: + # The location does not have this axis? + pass + + # Retrieve the user location (weightClass/widthClass) + # by going through the axis mapping in reverse. + user_loc = design_loc + mapping = None + for axis in designspace.axes: + if axis.tag == axis_def.tag: + mapping = axis.map + if mapping: + reverse_mapping = [(dl, ul) for ul, dl in mapping] + user_loc = interp(reverse_mapping, design_loc) + + if user_loc is not None: + axis_def.set_ufo_user_loc(ufo, user_loc) + else: + axis_def.set_ufo_user_loc(ufo, axis_def.default_user_loc) + + +def set_weight_class(ufo, designspace, instance): + """ the `weightClass` instance attribute from the UFO lib, and set + the ufo.info.openTypeOS2WeightClass accordingly. + """ + _set_class_from_instance(ufo, designspace, instance, WEIGHT_AXIS_DEF) + + +def set_width_class(ufo, designspace, instance): + """Read the `widthClass` instance attribute from the UFO lib, and set the + ufo.info.openTypeOS2WidthClass accordingly. + """ + _set_class_from_instance(ufo, designspace, instance, WIDTH_AXIS_DEF) + + +def apply_instance_data(designspace): + """Open instances, apply data, and re-save. + + Args: + instance_data: DesignSpaceDocument object with some instances + Returns: + List of opened and updated instance UFOs. + """ + import defcon + + instance_ufos = [] + for instance in designspace.instances: + path = instance.path + ufo = defcon.Font(path) + set_weight_class(ufo, designspace, instance) + set_width_class(ufo, designspace, instance) + + glyphs_instance = InstanceDescriptorAsGSInstance(instance) + builder = UFOBuilder(instance, defcon) + # to_ufo_custom_params(self, ufo, data.parent) # FIXME: (jany) needed? + to_ufo_custom_params(self, ufo, instance) + ufo.save() + instance_ufos.append(ufo) + return instance_ufos diff --git a/Lib/glyphsLib/classes.py b/Lib/glyphsLib/classes.py index e99ec6faa..1a055fb54 100755 --- a/Lib/glyphsLib/classes.py +++ b/Lib/glyphsLib/classes.py @@ -3013,7 +3013,7 @@ def __repr__(self): return "<%s \"%s\">" % (self.__class__.__name__, self.familyName) def shouldWriteValueForKey(self, key): - if key in ("unitsPerEm", "versionMinor"): + if key in ("unitsPerEm", "versionMajor", "versionMinor"): return True return super(GSFont, self).shouldWriteValueForKey(key) diff --git a/Lib/glyphsLib/interpolation.py b/Lib/glyphsLib/interpolation.py index cfd3ccf08..e605f3ef7 100644 --- a/Lib/glyphsLib/interpolation.py +++ b/Lib/glyphsLib/interpolation.py @@ -24,7 +24,7 @@ from glyphsLib.builder.custom_params import to_ufo_custom_params from glyphsLib.builder.names import build_stylemap_names from glyphsLib.builder.constants import GLYPHS_PREFIX -from glyphsLib.classes import WEIGHT_CODES, WIDTH_CODES +from glyphsLib.builder.instances import apply_instance_data from glyphsLib.util import build_ufo_path, write_ufo, clean_ufo @@ -41,7 +41,7 @@ def interpolate(ufos, master_dir, out_dir, instance_data, round_geometry=True): """ # TODO: (jany) This should not be in glyphsLib, but rather an instance # method of the designspace document, or another thing like - # ufoProcessor. + # ufoProcessor/mutatorMath.build() # GlyphsLib should put all that is necessary to interpolate into the # InstanceDescriptor (lib if needed) # All the logic like applying custom parameters and so on should be made @@ -61,52 +61,3 @@ def build_designspace(masters, master_dir, out_dir, instance_data): """ # TODO: (jany) check whether this function is still useful raise NotImplementedError - - -def _set_class_from_instance(ufo, data, key, codes): - class_name = getattr(data, key) - if class_name: - ufo.lib[GLYPHS_PREFIX + key + "Class"] = class_name - if class_name in codes: - class_code = codes[class_name] - ufo_key = "".join(['openTypeOS2', key[0].upper(), key[1:], 'Class']) - setattr(ufo.info, ufo_key, class_code) - - -def set_weight_class(ufo, instance_data): - """ Store `weightClass` instance attributes in the UFO lib, and set the - ufo.info.openTypeOS2WeightClass accordingly. - """ - _set_class_from_instance(ufo, instance_data, "weight", WEIGHT_CODES) - - -def set_width_class(ufo, instance_data): - """ Store `widthClass` instance attributes in the UFO lib, and set the - ufo.info.openTypeOS2WidthClass accordingly. - """ - _set_class_from_instance(ufo, instance_data, "width", WIDTH_CODES) - - -def apply_instance_data(instance_data): - """Open instances, apply data, and re-save. - - Args: - instance_data: List of (path, data) tuples, one for each instance. - Returns: - List of opened and updated instance UFOs. - """ - # FIXME: (jany) This is implemented because fontmake calls it. - # The instance_data will be an array of InstanceDescriptors - import defcon - - instance_ufos = [] - for path, data in instance_data: - ufo = defcon.Font(path) - set_weight_class(ufo, data) - set_width_class(ufo, data) - self = UFOBuilder(instance_data, defcon) - # to_ufo_custom_params(self, ufo, data.parent) # FIXME: (jany) needed? - to_ufo_custom_params(self, ufo, data) - ufo.save() - instance_ufos.append(ufo) - return instance_ufos diff --git a/tests/builder/interpolation_test.py b/tests/builder/interpolation_test.py index 7383eeba8..1bacf093a 100644 --- a/tests/builder/interpolation_test.py +++ b/tests/builder/interpolation_test.py @@ -27,7 +27,7 @@ import defcon from fontTools.misc.py23 import open from glyphsLib.builder.constants import GLYPHS_PREFIX -from glyphsLib.interpolation import set_weight_class, set_width_class +from glyphsLib.builder.instances import set_weight_class, set_width_class from glyphsLib.classes import GSFont, GSFontMaster, GSInstance from glyphsLib import to_designspace, to_glyphs @@ -122,6 +122,14 @@ def makeInstance(name, weight=None, width=None, is_bold=None, is_italic=None, return inst +def makeInstanceDescriptor(*args, **kwargs): + """Same as makeInstance but return the corresponding InstanceDescriptor.""" + ginst = makeInstance(*args, **kwargs) + font = makeFont([makeMaster('Regular')], [ginst], 'Family') + doc = to_designspace(font) + return doc, doc.instances[0] + + def makeFont(masters, instances, familyName): font = GSFont() font.familyName = familyName @@ -390,86 +398,90 @@ def test_no_weigth_class(self): ufo = defcon.Font() # name here says "Bold", however no excplit weightClass # is assigned - set_weight_class(ufo, makeInstance("Bold")) + doc, instance = makeInstanceDescriptor("Bold") + set_weight_class(ufo, doc, instance) # the default OS/2 weight class is set self.assertEqual(ufo.info.openTypeOS2WeightClass, 400) # non-empty value is stored in the UFO lib even if same as default - self.assertEqual(ufo.lib[WEIGHT_CLASS_KEY], "Regular") + # FIXME: (jany) why do we want to write something Glyphs-specific in + # the instance UFO? Does someone later in the toolchain rely on it? + # self.assertEqual(ufo.lib[WEIGHT_CLASS_KEY], "Regular") def test_weight_class(self): ufo = defcon.Font() - data = makeInstance( + doc, data = makeInstanceDescriptor( "Bold", weight=("Bold", None, 150) ) - set_weight_class(ufo, data) + set_weight_class(ufo, doc, data) self.assertEqual(ufo.info.openTypeOS2WeightClass, 700) - self.assertEqual(ufo.lib[WEIGHT_CLASS_KEY], "Bold") + # self.assertEqual(ufo.lib[WEIGHT_CLASS_KEY], "Bold") def test_explicit_default_weight(self): ufo = defcon.Font() - data = makeInstance( + doc, data = makeInstanceDescriptor( "Regular", weight=("Regular", None, 100) ) - set_weight_class(ufo, data) + set_weight_class(ufo, doc, data) # the default OS/2 weight class is set self.assertEqual(ufo.info.openTypeOS2WeightClass, 400) # non-empty value is stored in the UFO lib even if same as default - self.assertEqual(ufo.lib[WEIGHT_CLASS_KEY], "Regular") + # self.assertEqual(ufo.lib[WEIGHT_CLASS_KEY], "Regular") def test_no_width_class(self): ufo = defcon.Font() # no explicit widthClass set, instance name doesn't matter - set_width_class(ufo, makeInstance("Normal")) + doc, data = makeInstanceDescriptor("Normal") + set_width_class(ufo, doc, data) # the default OS/2 width class is set self.assertEqual(ufo.info.openTypeOS2WidthClass, 5) # non-empty value is stored in the UFO lib even if same as default - self.assertEqual(ufo.lib[WIDTH_CLASS_KEY], "Medium (normal)") + # self.assertEqual(ufo.lib[WIDTH_CLASS_KEY], "Medium (normal)") def test_width_class(self): ufo = defcon.Font() - data = makeInstance( + doc, data = makeInstanceDescriptor( "Condensed", width=("Condensed", 3, 80) ) - set_width_class(ufo, data) + set_width_class(ufo, doc, data) self.assertEqual(ufo.info.openTypeOS2WidthClass, 3) - self.assertEqual(ufo.lib[WIDTH_CLASS_KEY], "Condensed") + # self.assertEqual(ufo.lib[WIDTH_CLASS_KEY], "Condensed") def test_explicit_default_width(self): ufo = defcon.Font() - data = makeInstance( + doc, data = makeInstanceDescriptor( "Regular", width=("Medium (normal)", 5, 100) ) - set_width_class(ufo, data) + set_width_class(ufo, doc, data) # the default OS/2 width class is set self.assertEqual(ufo.info.openTypeOS2WidthClass, 5) # non-empty value is stored in the UFO lib even if same as default - self.assertEqual(ufo.lib[WIDTH_CLASS_KEY], "Medium (normal)") + # self.assertEqual(ufo.lib[WIDTH_CLASS_KEY], "Medium (normal)") def test_weight_and_width_class(self): ufo = defcon.Font() - data = makeInstance( + doc, data = makeInstanceDescriptor( "SemiCondensed ExtraBold", weight=("ExtraBold", None, 160), width=("SemiCondensed", 4, 90) ) - set_weight_class(ufo, data) - set_width_class(ufo, data) + set_weight_class(ufo, doc, data) + set_width_class(ufo, doc, data) self.assertEqual(ufo.info.openTypeOS2WeightClass, 800) - self.assertEqual(ufo.lib[WEIGHT_CLASS_KEY], "ExtraBold") + # self.assertEqual(ufo.lib[WEIGHT_CLASS_KEY], "ExtraBold") self.assertEqual(ufo.info.openTypeOS2WidthClass, 4) - self.assertEqual(ufo.lib[WIDTH_CLASS_KEY], "SemiCondensed") + # self.assertEqual(ufo.lib[WIDTH_CLASS_KEY], "SemiCondensed") def test_unknown_weight_class(self): ufo = defcon.Font() @@ -479,16 +491,37 @@ def test_unknown_weight_class(self): # NOTE It is not possible from the user interface to set a custom # string as instance 'weightClass' since the choice is constrained # by a drop-down menu. - data = makeInstance( + doc, data = makeInstanceDescriptor( "DemiLight Italic", weight=("DemiLight", 350, 70) ) - set_weight_class(ufo, data) + set_weight_class(ufo, doc, data) + + # Here we have set the weightClass to 350 so even though the string + # is wrong, our value of 350 should be used. + self.assertTrue(ufo.info.openTypeOS2WeightClass == 350) + + def test_unknown_weight_class(self): + ufo = defcon.Font() + # "DemiLight" is not among the predefined weight classes listed in + # Glyphs.app/Contents/Frameworks/GlyphsCore.framework/Versions/A/ + # Resources/weights.plist + # NOTE It is not possible from the user interface to set a custom + # string as instance 'weightClass' since the choice is constrained + # by a drop-down menu. + doc, data = makeInstanceDescriptor( + "DemiLight Italic", + weight=("DemiLight", None, 70) + ) + + set_weight_class(ufo, doc, data) # we do not set any OS/2 weight class; user needs to provide # a 'weightClass' custom parameter in this special case - self.assertTrue(ufo.info.openTypeOS2WeightClass is None) + # FIXME: (jany) the new code writes the default OS2 class instead of + # None. Is that a problem? + self.assertTrue(ufo.info.openTypeOS2WeightClass == 400) # def test_designspace_roundtrip(tmpdir): diff --git a/tests/writer_test.py b/tests/writer_test.py index dc74e659b..aa41e157a 100644 --- a/tests/writer_test.py +++ b/tests/writer_test.py @@ -242,6 +242,13 @@ def test_write_font_attributes(self): written = test_helpers.write_to_lines(font) self.assertFalse(any("keyboardIncrement" in line for line in written)) + # Always write versionMajor and versionMinor, even when 0 + font.versionMajor = 0 + font.versionMinor = 0 + written = test_helpers.write_to_lines(font) + self.assertIn("versionMajor = 0;", written) + self.assertIn("versionMinor = 0;", written) + def test_write_font_master_attributes(self): """Test the writer on all GSFontMaster attributes""" master = classes.GSFontMaster()