From 44f74dc8bb476c84f8648fee9067951cf83603bc Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Fri, 10 May 2019 16:32:11 +0100 Subject: [PATCH] designspaceLib: add loadSourceFonts method with custom opener Allows to load the SourceDescriptor.font attribute from its path, using a custom callable (e.g. defcon.Font or ttLib.TTFont, etc.). --- Lib/fontTools/designspaceLib/__init__.py | 38 ++++++++++++++++++++++ Tests/designspaceLib/designspace_test.py | 40 ++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/Lib/fontTools/designspaceLib/__init__.py b/Lib/fontTools/designspaceLib/__init__.py index 1aef152f5..121f6b30c 100644 --- a/Lib/fontTools/designspaceLib/__init__.py +++ b/Lib/fontTools/designspaceLib/__init__.py @@ -1262,3 +1262,41 @@ class DesignSpaceDocument(LogMixin, AsDictMixin): newConditions.append(dict(name=cond['name'], minimum=minimum, maximum=maximum)) newConditionSets.append(newConditions) rule.conditionSets = newConditionSets + + def loadSourceFonts(self, opener, **kwargs): + """Ensure SourceDescriptor.font attributes are loaded, and return list of fonts. + + Takes a callable which initializes a new font object (e.g. TTFont, or + defcon.Font, etc.) from the SourceDescriptor.path, and sets the + SourceDescriptor.font attribute. + If the font attribute is already not None, it is not loaded again. + Fonts with the same path are only loaded once and shared among SourceDescriptors. + + Args: + opener (Callable): takes one required positional argument, the source.path, + and an optional list of keyword arguments, and returns a new font object + loaded from the path. + **kwargs: extra options passed on to the opener function. + + Returns: + List of font objects in the order they appear in the sources list. + """ + # we load fonts with the same source.path only once + loaded = {} + fonts = [] + for source in self.sources: + if source.font is not None: # font already loaded + fonts.append(source.font) + continue + if source.path in loaded: + source.font = loaded[source.path] + else: + if source.path is None: + raise DesignSpaceDocumentError( + "Designspace source '%s' has no 'path' attribute" + % (source.name or "") + ) + source.font = opener(source.path, **kwargs) + loaded[source.path] = source.font + fonts.append(source.font) + return fonts diff --git a/Tests/designspaceLib/designspace_test.py b/Tests/designspaceLib/designspace_test.py index cdbf08b14..608ffb1c4 100644 --- a/Tests/designspaceLib/designspace_test.py +++ b/Tests/designspaceLib/designspace_test.py @@ -13,6 +13,7 @@ from fontTools.misc import plistlib from fontTools.designspaceLib import ( DesignSpaceDocument, SourceDescriptor, AxisDescriptor, RuleDescriptor, InstanceDescriptor, evaluateRule, processRules, posix, DesignSpaceDocumentError) +from fontTools import ttLib def _axesAsDict(axes): """ @@ -909,3 +910,42 @@ def test_findDefault_axis_mapping(): designspace.axes[1].default = 0 assert designspace.findDefault().filename == "Font-Regular.ufo" + + +def test_loadSourceFonts(): + + def opener(path): + font = ttLib.TTFont() + font.importXML(path) + return font + + # this designspace file contains .TTX source paths + path = os.path.join( + os.path.dirname(os.path.dirname(__file__)), + "varLib", + "data", + "SparseMasters.designspace" + ) + designspace = DesignSpaceDocument.fromfile(path) + + # force two source descriptors to have the same path + designspace.sources[1].path = designspace.sources[0].path + + fonts = designspace.loadSourceFonts(opener) + + assert len(fonts) == 3 + assert all(isinstance(font, ttLib.TTFont) for font in fonts) + assert fonts[0] is fonts[1] # same path, identical font object + + fonts2 = designspace.loadSourceFonts(opener) + + for font1, font2 in zip(fonts, fonts2): + assert font1 is font2 + + +def test_loadSourceFonts_no_required_path(): + designspace = DesignSpaceDocument() + designspace.sources.append(SourceDescriptor()) + + with pytest.raises(DesignSpaceDocumentError, match="no 'path' attribute"): + designspace.loadSourceFonts(lambda p: p)