diff --git a/Lib/glyphsLib/builder/custom_params.py b/Lib/glyphsLib/builder/custom_params.py index 7fc68d3c2..ad024fb6b 100644 --- a/Lib/glyphsLib/builder/custom_params.py +++ b/Lib/glyphsLib/builder/custom_params.py @@ -35,6 +35,7 @@ ) from .features import replace_feature, replace_prefixes from glyphsLib.classes import GSCustomParameter, GSFont, GSFontMaster, GSInstance +from .instances import InstanceDescriptorAsGSInstance """Set Glyphs custom parameters in UFO info or lib, where appropriate. @@ -161,7 +162,7 @@ def __init__( self.write_to_ufo = write_to_ufo self.write_to_glyphs = write_to_glyphs self.glyphs_owner_class = glyphs_owner_class - + def __repr__(self): return "<{} {}>".format(self.__class__.__name__, self.glyphs_name) @@ -317,7 +318,7 @@ def register_property_handler(handler): register_parameter_handler(ParamHandler(glyphs_name, ufo_name, glyphs_long_name=ufo_name, glyphs_owner_class=GSFontMaster)) for glyphs_name, ufo_name in GLYPHS_INSTANCE_UFO_CUSTOM_PARAMS: - register_parameter_handler(ParamHandler(glyphs_name, ufo_name, glyphs_long_name=ufo_name, glyphs_owner_class=GSInstance)) + register_parameter_handler(ParamHandler(glyphs_name, ufo_name, glyphs_long_name=ufo_name, glyphs_owner_class=(GSInstance, InstanceDescriptorAsGSInstance))) # Reference: # https://github.com/googlefonts/glyphsLib/pull/881#issuecomment-1474226616 @@ -476,7 +477,7 @@ def to_ufo(self, builder, glyphs, ufo): # FIXME: (georg) This is actually not working as the handlers is not applied to instances, yet. There is custom code for glyphs_name in ("weightClass", "widthClass"): ufo_name = "openTypeOS2W" + glyphs_name[1:] - register_parameter_handler(ParamHandler(glyphs_name, ufo_name, value_to_ufo=int, glyphs_owner_class=GSInstance)) + register_parameter_handler(ParamHandler(glyphs_name, ufo_name, value_to_ufo=int, glyphs_owner_class=(GSInstance, InstanceDescriptorAsGSInstance))) # convert Glyphs' GASP Table to UFO openTypeGaspRangeRecords def to_ufo_gasp_table(value): @@ -759,7 +760,7 @@ class OS2SelectionParamHandler(AbstractParamHandler): glyphs_name = None ufo_name = "openTypeOS2Selection" flags = {7: "Use Typo Metrics", 8: "Has WWS Names"} - glyphs_owner_class=(GSFont, GSInstance) + glyphs_owner_class=(GSFont, GSInstance, InstanceDescriptorAsGSInstance) # Note that en empty openTypeOS2Selection list should stay an empty list, as # opposed to a non-existant list. In the latter case, we round-trip nothing, in the # former, we at least write an empty list to openTypeOS2SelectionUnsupportedBits @@ -846,7 +847,7 @@ class FilterParamHandler(AbstractParamHandler): +----+-+ | | | +-+-----------+ +-+----------+ - |GSFontMaster | |GSInstance | + |GSFontMaster | |GSInstance | +-------------+ +------------+ userData customParameters com...ufo2ft.filters Filter & PreFilter diff --git a/Lib/glyphsLib/builder/font.py b/Lib/glyphsLib/builder/font.py index 9d024ad35..0db2436b6 100644 --- a/Lib/glyphsLib/builder/font.py +++ b/Lib/glyphsLib/builder/font.py @@ -41,7 +41,9 @@ def to_ufo_font_attributes(self, family_name): font = self.font disableAllAutomaticBehaviour = False - disableAllAutomaticBehaviourParameter = font.customParameters["DisableAllAutomaticBehaviour"] + disableAllAutomaticBehaviourParameter = font.customParameters[ + "DisableAllAutomaticBehaviour" + ] if disableAllAutomaticBehaviourParameter: disableAllAutomaticBehaviour = disableAllAutomaticBehaviourParameter for index, master in enumerate(font.masters): @@ -54,23 +56,44 @@ def to_ufo_font_attributes(self, family_name): self.to_ufo_names(ufo, master, family_name) # .names self.to_ufo_family_user_data(ufo) # .user_data - if has_any_corner_components(font, master): - ufo.lib.setdefault(UFO2FT_FILTERS_KEY, []).append( - { - "namespace": "glyphsLib.filters", - "name": "cornerComponents", - "pre": True, - } - ) - if not disableAllAutomaticBehaviour: - ufo.lib.setdefault(UFO2FT_FILTERS_KEY, []).append( - {"namespace": "glyphsLib.filters", "name": "eraseOpenCorners", "pre": True} - ) self.to_ufo_properties(ufo, font) self.to_ufo_custom_params(ufo, font, "font") # .custom_params self.to_ufo_custom_params(ufo, master, "fontMaster") # .custom_params self.to_ufo_master_attributes(ufo, master) # .masters + # Extract nested lib keys to the top level + nestedUserData = ufo.lib.get("com.schriftgestaltung.fontMaster.userData", {}) + if UFO2FT_FILTERS_KEY not in ufo.lib and UFO2FT_FILTERS_KEY in nestedUserData: + ufo.lib[UFO2FT_FILTERS_KEY] = nestedUserData[UFO2FT_FILTERS_KEY] + + del nestedUserData[UFO2FT_FILTERS_KEY] + if not nestedUserData: + del ufo.lib["com.schriftgestaltung.fontMaster.userData"] + + if not disableAllAutomaticBehaviour: + if UFO2FT_FILTERS_KEY not in ufo.lib: + ufo.lib[UFO2FT_FILTERS_KEY] = [ + { + "namespace": "glyphsLib.filters", + "name": "eraseOpenCorners", + "pre": True, + } + ] + + if has_any_corner_components(font, master): + filters = ufo.lib.setdefault(UFO2FT_FILTERS_KEY, []) + if not any( + hasattr(f, "get") and f.get("name") == "cornerComponents" + for f in filters + ): + filters.append( + { + "namespace": "glyphsLib.filters", + "name": "cornerComponents", + "pre": True, + } + ) + ufo.lib[MASTER_ORDER_LIB_KEY] = index # FIXME: (jany) in the future, yield this UFO (for memory, lazy iter) @@ -90,14 +113,14 @@ def to_ufo_font_attributes(self, family_name): "compatibleFullNames": "openTypeNameCompatibleFullName", "copyrights": "copyright", "descriptions": "openTypeNameDescription", - "designers" : "openTypeNameDesigner", + "designers": "openTypeNameDesigner", "designerURL": "openTypeNameDesignerURL", - #"familyNames": "familyName", + # "familyNames": "familyName", "preferredFamilyNames": "openTypeNamePreferredFamilyName", "preferredSubfamilyNames": "openTypeNamePreferredSubfamilyName", "licenses": "openTypeNameLicense", "licenseURL": "openTypeNameLicenseURL", - "manufacturers": "openTypeNameManufacturer", + "manufacturers": "openTypeNameManufacturer", "manufacturerURL": "openTypeNameManufacturerURL", "postscriptFontName": "postscriptFontName", "postscriptFullNames": "postscriptFullName", @@ -136,8 +159,8 @@ def to_ufo_metadata(master, ufo): # by a glyphOrder custom parameter below in `to_ufo_custom_params`. ufo.glyphOrder = list(glyph.name for glyph in font.glyphs) + def to_glyphs_metadata(ufo, font): - for glyphs_key, ufo_key in PROPERTIES_FIELDS.items(): value = getattr(ufo.info, ufo_key) if value: @@ -252,10 +275,7 @@ def _original_master_order(source): def has_any_corner_components(font, master): for glyph in font.glyphs: for layerId, layer in glyph._layers.items(): - if ( - layer.associatedMasterId != master.id - or not layer.hints - ): + if layer.associatedMasterId != master.id or not layer.hints: continue if layer.hasCorners: return True diff --git a/Lib/glyphsLib/builder/instances.py b/Lib/glyphsLib/builder/instances.py index 58a193ad2..30aac553d 100644 --- a/Lib/glyphsLib/builder/instances.py +++ b/Lib/glyphsLib/builder/instances.py @@ -40,7 +40,6 @@ PROPERTIES_KEY, ) from .names import build_stylemap_names -from .custom_params import to_ufo_custom_params, to_ufo_properties logger = logging.getLogger(__name__) @@ -109,7 +108,7 @@ def _to_designspace_instance(self, instance): ufo_instance.lib["openTypeOS2WidthClass"] = instance.widthClass ufo_instance.lib["openTypeOS2WeightClass"] = instance.weightClass - + uniqueID = instance.customParameters["uniqueID"] if uniqueID: ufo_instance.lib["openTypeNameUniqueID"] = uniqueID @@ -171,7 +170,7 @@ def _to_properties(instance): return [ (item.name, item.value) for item in instance.properties - if item.name not in CUSTOM_PARAMETERS_BLACKLIST + if item.name not in CUSTOM_PARAMETERS_BLACKLIST ] @@ -287,7 +286,7 @@ def to_glyphs_instances(self): # noqa: C901 if ufo_instance.filename and self.minimize_ufo_diffs: instance.customParameters[UFO_FILENAME_CUSTOM_PARAM] = ufo_instance.filename - + # some info that needs to be in a instance in Glyphs is stored in the sources. So we try to find a matching source (FIXME: (georg) not nice for source in self.designspace.sources: if source.location == ufo_instance.location: @@ -384,6 +383,9 @@ def apply_instance_data_to_ufo(ufo, instance, designspace): Returns: None. """ + # Import here to prevent a cyclic import with custom_params + from .custom_params import to_ufo_custom_params, to_ufo_properties + try: ufo.info.openTypeOS2WidthClass = instance.lib["openTypeOS2WidthClass"] except: diff --git a/tests/builder/custom_params_test.py b/tests/builder/custom_params_test.py index 87934d621..2b67e7a1a 100644 --- a/tests/builder/custom_params_test.py +++ b/tests/builder/custom_params_test.py @@ -40,14 +40,20 @@ UFO_FILENAME_KEY, FULL_FILENAME_KEY, ) -from glyphsLib.classes import GSFont, GSFontMaster, GSCustomParameter, GSGlyph, GSLayer +from glyphsLib.classes import ( + GSFont, + GSFontMaster, + GSInstance, + GSCustomParameter, + GSGlyph, + GSLayer, +) from glyphsLib.types import parse_datetime DATA = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data") class SetCustomParamsTestBase(object): - ufo_module = None # subclasses must override this def setUp(self): @@ -55,12 +61,15 @@ def setUp(self): self.font = GSFont() self.master = GSFontMaster() self.font.masters.insert(0, self.master) + self.instance = GSInstance() + self.font.instances.insert(0, self.instance) self.builder = UFOBuilder(self.font) def set_custom_params(self): self.builder.to_ufo_properties(self.ufo, self.font) self.builder.to_ufo_custom_params(self.ufo, self.font, "font") self.builder.to_ufo_custom_params(self.ufo, self.master, "fontMaster") + self.builder.to_ufo_custom_params(self.ufo, self.instance, "instance") def test_normalizes_curved_quotes_in_names(self): self.master.customParameters = [ @@ -68,7 +77,9 @@ def test_normalizes_curved_quotes_in_names(self): GSCustomParameter(name="“also bad”", value=2), ] self.set_custom_params() - custom_parameters = self.ufo.lib["com.schriftgestaltung.fontMaster.customParameters"] + custom_parameters = self.ufo.lib[ + "com.schriftgestaltung.fontMaster.customParameters" + ] self.assertEqual(custom_parameters[0]["name"], "'bad'") self.assertEqual(custom_parameters[1]["name"], '"also bad"') @@ -89,7 +100,7 @@ def test_set_fsSelection_flags_empty(self): self.assertEqual(self.font.customParameters["Use Typo Metrics"], None) self.assertEqual(self.font.customParameters["Has WWS Names"], None) self.assertEqual( - self.font.customParameters["openTypeOS2SelectionUnsupportedBits"], [] + self.font.customParameters["openTypeOS2SelectionUnsupportedBits"], None ) self.set_custom_params() self.assertEqual(self.ufo.info.openTypeOS2Selection, []) @@ -109,16 +120,16 @@ def test_set_fsSelection_flags_all(self): def test_set_fsSelection_flags(self): self.assertEqual(self.ufo.info.openTypeOS2Selection, None) - self.master.customParameters["Has WWS Names"] = False + self.font.customParameters["Has WWS Names"] = False self.set_custom_params() self.assertEqual(self.ufo.info.openTypeOS2Selection, None) - self.master.customParameters["Use Typo Metrics"] = True + self.font.customParameters["Use Typo Metrics"] = True self.set_custom_params() self.assertEqual(self.ufo.info.openTypeOS2Selection, [7]) self.ufo = self.ufo_module.Font() - self.master.customParameters = [ + self.font.customParameters = [ GSCustomParameter(name="Use Typo Metrics", value=True), GSCustomParameter(name="Has WWS Names", value=True), ] @@ -162,7 +173,7 @@ def test_parse_glyphs_filter(self, mock_parse_glyphs_filter): pre_filter = "AddExtremes" filter1 = "Transformations;OffsetX:40;OffsetY:60;include:uni0334,uni0335" filter2 = "Transformations;OffsetX:10;OffsetY:-10;exclude:uni0334,uni0335" - self.master.customParameters.extend( + self.instance.customParameters.extend( [ GSCustomParameter(name="PreFilter", value=pre_filter), GSCustomParameter(name="Filter", value=filter1), @@ -201,7 +212,9 @@ def test_set_codePageRanges(self): self.set_custom_params() self.assertEqual(self.ufo.info.openTypeOS2CodePageRanges, [0, 1, 15]) self.font = glyphsLib.to_glyphs([self.ufo], minimize_ufo_diffs=True) - self.assertEqual(self.font.customParameters["codePageRanges"], ["1252", "1250", "bit 15"]) + self.assertEqual( + self.font.customParameters["codePageRanges"], ["1252", "1250", "bit 15"] + ) def test_set_openTypeOS2CodePageRanges(self): self.font.customParameters["openTypeOS2CodePageRanges"] = ["1252", "1250"] @@ -209,7 +222,9 @@ def test_set_openTypeOS2CodePageRanges(self): self.set_custom_params() self.assertEqual(self.ufo.info.openTypeOS2CodePageRanges, [0, 1, 15]) self.font = glyphsLib.to_glyphs([self.ufo], minimize_ufo_diffs=True) - self.assertEqual(self.font.customParameters["codePageRanges"], ["1252", "1250", "bit 15"]) + self.assertEqual( + self.font.customParameters["codePageRanges"], ["1252", "1250", "bit 15"] + ) self.assertIsNone(self.font.customParameters["codePageRangesUnsupportedBits"]) def test_gasp_table(self): @@ -257,7 +272,9 @@ def test_xHeight(self): self.master.customParameters["xHeight"] = "500" self.set_custom_params() # Additional xHeight values are Glyphs-specific and stored in lib - custom_parameters = self.ufo.lib["com.schriftgestaltung.fontMaster.customParameters"] + custom_parameters = self.ufo.lib[ + "com.schriftgestaltung.fontMaster.customParameters" + ] self.assertEqual(custom_parameters[0]["name"], "xHeight") self.assertEqual(custom_parameters[0]["value"], "500") # The xHeight from the property is not modified @@ -285,7 +302,7 @@ def test_replace_feature(self): repl = "liga; sub f f by ff;" - self.master.customParameters["Replace Feature"] = repl + self.instance.customParameters["Replace Feature"] = repl self.set_custom_params() self.assertEqual( @@ -311,7 +328,7 @@ def test_replace_feature(self): original = self.ufo.features.text repl = "numr; sub one by one.numr;\nsub two by two.numr;\n" - self.master.customParameters["Replace Feature"] = repl + self.instance.customParameters["Replace Feature"] = repl self.set_custom_params() self.assertEqual(self.ufo.features.text, original) @@ -345,10 +362,10 @@ def test_replace_prefix(self): """ ) - self.master.customParameters.append( + self.instance.customParameters.append( GSCustomParameter("Replace Prefix", "FOO; include(../foo.fea);") ) - self.master.customParameters.append( + self.instance.customParameters.append( GSCustomParameter("Replace Prefix", "BAR; include(../bar.fea);") ) self.set_custom_params() @@ -384,9 +401,17 @@ def test_replace_prefix(self): ), ) - def test_useProductionNames(self): + def test_useProductionNames_font(self): + for value in (True, False): + self.font.customParameters["Don't use Production Names"] = value + self.set_custom_params() + + self.assertIn(UFO2FT_USE_PROD_NAMES_KEY, self.ufo.lib) + self.assertEqual(self.ufo.lib[UFO2FT_USE_PROD_NAMES_KEY], not value) + + def test_useProductionNames_instance(self): for value in (True, False): - self.master.customParameters["Don't use Production Names"] = value + self.instance.customParameters["Don't use Production Names"] = value self.set_custom_params() self.assertIn(UFO2FT_USE_PROD_NAMES_KEY, self.ufo.lib) @@ -416,23 +441,49 @@ def test_version_string(self): self.set_custom_params() self.assertEqual(self.ufo.info.openTypeNameVersion, "Version 2.040") - @pytest.mark.xfail - def test_ufo2ft_filter_roundtrip(self): + def test_ufo2ft_filter_glyphs_to_ufo(self): + # Test the one-way conversion of (Pre)Filters into ufo2ft filters. + # See the docstring for FilterParamHandler. + # This first test uses a ufo2ft-specific filter, propagateAnchors + glyphs_filter = "propagateAnchors;include:a,b,c" ufo_filters = [ {"name": "propagateAnchors", "pre": True, "include": ["a", "b", "c"]} ] - glyphs_filter = "propagateAnchors;include:a,b,c" + self.instance.customParameters["PreFilter"] = glyphs_filter + self.set_custom_params() + self.assertEqual(self.ufo.lib[UFO2FT_FILTERS_KEY], ufo_filters) - # Test the one-way conversion of (Pre)Filters into ufo2ft filters. See the - # docstring for FilterParamHandler. - self.master.customParameters["PreFilter"] = glyphs_filter + def test_Glyphsapp_filter_glyphs_to_ufo(self): + # Test the one-way conversion of (Pre)Filters into ufo2ft filters. + # See the docstring for FilterParamHandler. + # This second test uses a Glyphs.app-specific filter, RoundCorners + glyphs_filter = "RoundCorners;20;include:a,b,c" + ufo_filters = [ + { + "name": "RoundCorners", + "pre": True, + "include": ["a", "b", "c"], + "args": [20], + } + ] + self.instance.customParameters["PreFilter"] = glyphs_filter self.set_custom_params() self.assertEqual(self.ufo.lib[UFO2FT_FILTERS_KEY], ufo_filters) + def test_ufo2ft_filter_ufo_to_glyphs_to_ufo(self): # Test the round-tripping of ufo2ft filters from UFO -> Glyphs master -> UFO. # See the docstring for FilterParamHandler. + ufo_filters = [ + {"name": "whateverUfo2FtCanDo", "pre": True, "include": ["a", "b", "c"]} + ] + self.ufo.lib[UFO2FT_FILTERS_KEY] = ufo_filters + + # While it doesn't make sense for Glyphs.app to have filters on a + # GSFontMaster, we still want to put them there to match the UFO + # workflow. It's fine that it doesn't make sense in Glyphs.app because + # anyway it's userData; Glyphs.app is not expected to make sense of it + # or apply it to anything. font_rt = glyphsLib.to_glyphs([self.ufo]) - # FIXME: (georg) above it is put into customParameters and after rt, it still is but we expect it not to be? self.assertNotIn("PreFilter", font_rt.masters[0].customParameters) self.assertEqual(font_rt.masters[0].userData[UFO2FT_FILTERS_KEY], ufo_filters) ufo_rt = glyphsLib.to_ufos(font_rt, ufo_module=self.ufo_module)[0] @@ -629,7 +680,7 @@ def test_ufo_opentype_name_preferred_family_subfamily_name(): for filename in filenames: file = glyphsLib.GSFont(os.path.join(DATA, filename)) instance = file.instances[0] - + actual = instance.properties["preferredFamilyNames"] assert actual == "Typographic New Font", filename @@ -703,6 +754,9 @@ def test_ufo_opentype_name_records(): def test_ufo_opentype_os2_selection(): + """Bit 7 comes from the "Use Typo Metrics" param on the font. + Bit 8 comes from the "Has WWS Names" param on the instance. + """ from glyphsLib.interpolation import apply_instance_data_to_ufo filenames = [ @@ -743,3 +797,12 @@ def test_mutiple_params(ufo_module): assert instance.customParameters[0].value == "ccmp;sub space by space;" assert instance.customParameters[1].value == "liga;sub space space by space;" + + +def test_font_params_go_to_GSFont_instance_to_GSInstance(): + """TODO: if the custom params are registered on the GSFont, they should go + only to the default UFO of the designspace, and if they're registered on the + GSInstance, they should go only on the lib key of the + designspace. Same in the other direction. + """ + assert True