From 0efa653dbf4418c88e4b4234741337a42e15e1fc Mon Sep 17 00:00:00 2001 From: Frederik Berlaen Date: Tue, 20 May 2025 11:05:30 +0200 Subject: [PATCH 1/5] remove FontTool to list named instances --- drawBot/context/baseContext.py | 12 ++++---- drawBot/context/tools/variation.py | 48 ++++++++++++------------------ 2 files changed, 25 insertions(+), 35 deletions(-) diff --git a/drawBot/context/baseContext.py b/drawBot/context/baseContext.py index 3d035b9a..99985226 100644 --- a/drawBot/context/baseContext.py +++ b/drawBot/context/baseContext.py @@ -1894,7 +1894,7 @@ def fontNamedInstance(self, name: str, fontNameOrPath: str | SomePath | None = N fontName = self._font raise DrawBotError(f"Can not find instance with name: '{name}' for '{fontName}'.") - def listNamedInstances(self, fontNameOrPath: SomePath | None = None, fontNumber: int = 0) -> dict[str, dict]: + def listNamedInstances(self, fontNameOrPath: SomePath | None = None) -> dict[str, dict]: """ List all named instances from a variable font for the current font. @@ -1905,8 +1905,8 @@ def listNamedInstances(self, fontNameOrPath: SomePath | None = None, fontNumber: """ if fontNameOrPath is None: fontNameOrPath = self._font - font = getNSFontFromNameOrPath(fontNameOrPath, 10, fontNumber) - return variation.getNamedInstancesForFont(font) + descriptors = getFontDescriptorsFromPath(fontNameOrPath) + return variation.getNamedInstancesForDescriptors(descriptors) def tabs(self, tab: tuple[float, str] | None, *tabs: tuple[float, str]): """ @@ -3013,13 +3013,13 @@ def _getNSFontFromNameOrPath(fontNameOrPath, fontSize, fontNumber): @memoize def getFontDescriptorsFromPath(fontPath): + url = AppKit.NSURL.fileURLWithPath_(fontPath) + assert url is not None modTime = os.stat(fontPath).st_mtime prevModTime, descriptors = _reloadedFontDescriptors.get(fontPath, (modTime, None)) if modTime == prevModTime: if not descriptors: # Load font from disk, letting the OS handle caching and loading - url = AppKit.NSURL.fileURLWithPath_(fontPath) - assert url is not None descriptors = CoreText.CTFontManagerCreateFontDescriptorsFromURL(url) # Nothing was reloaded, this is the general case: do not cache the # descriptors globally (they are cached per newDrawing session via @@ -3032,7 +3032,7 @@ def getFontDescriptorsFromPath(fontPath): data = AppKit.NSData.dataWithContentsOfFile_(fontPath) descriptors = CoreText.CTFontManagerCreateFontDescriptorsFromData(data) _reloadedFontDescriptors[fontPath] = modTime, descriptors - return descriptors + return tuple(descriptors) def getFontName(font) -> str | None: diff --git a/drawBot/context/tools/variation.py b/drawBot/context/tools/variation.py index 8b15ad99..72d01f5a 100644 --- a/drawBot/context/tools/variation.py +++ b/drawBot/context/tools/variation.py @@ -49,43 +49,33 @@ def getVariationAxesForFont(font): @memoize -def getNamedInstancesForFont(font): +def getNamedInstancesForDescriptors(descriptors): """ Return a dict { postscriptName: location } of all named instances in a given font. """ - instances = OrderedDict() - if font is None: - return instances - fontDescriptor = font.fontDescriptor() - url = CoreText.CTFontDescriptorCopyAttribute(fontDescriptor, CoreText.kCTFontURLAttribute) - if url is None: - return instances + instances = dict() - variationAxesDescriptions = CoreText.CTFontCopyVariationAxes(font) - if variationAxesDescriptions is None: - # non-variable fonts have no named instances - return instances - tagNameMap = {} - for variationAxesDescription in variationAxesDescriptions: - tag = convertIntToVariationTag(variationAxesDescription[CoreText.kCTFontVariationAxisIdentifierKey]) - name = variationAxesDescription[CoreText.kCTFontVariationAxisNameKey] - tagNameMap[tag] = name + for descriptor in descriptors: + # convert to a ctFont + font = CoreText.CTFontCreateWithFontDescriptor(descriptor, 10, None) + # get the default variation axes + variationAxesDescriptions = CoreText.CTFontCopyVariationAxes(font) + + if variationAxesDescriptions is not None: + # variable fonts have named instances + fontVariation = CoreText.CTFontCopyVariation(font) + variationCoordinates = {} - ft = TTFont(url.path(), lazy=True, fontNumber=0) - if "fvar" in ft: - cgFont, _ = CoreText.CTFontCopyGraphicsFont(font, None) - fvar = ft["fvar"] + for variationAxesDescription in variationAxesDescriptions: + tag = convertIntToVariationTag(variationAxesDescription[CoreText.NSFontVariationAxisIdentifierKey]) + variationCoordinates[tag] = variationAxesDescription[CoreText.NSFontVariationAxisDefaultValueKey] - for instance in fvar.instances: - fontVariations = dict() - for axis, value in instance.coordinates.items(): - fontVariations[tagNameMap[axis]] = value + for fontVariationKey, fontVariationValue in fontVariation.items(): + tag = convertIntToVariationTag(fontVariationKey) + variationCoordinates[tag] = fontVariationValue - varFont = CoreText.CGFontCreateCopyWithVariations(cgFont, fontVariations) - postScriptName = CoreText.CGFontCopyPostScriptName(varFont) - instances[postScriptName] = instance.coordinates + instances[descriptor.postscriptName()] = variationCoordinates - ft.close() return instances From c98f1339ac21bc64a90dd4ee08e2947ae5bd6347 Mon Sep 17 00:00:00 2001 From: Frederik Berlaen Date: Tue, 20 May 2025 11:54:23 +0200 Subject: [PATCH 2/5] fix test for installed fonts --- drawBot/context/baseContext.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/drawBot/context/baseContext.py b/drawBot/context/baseContext.py index 99985226..6f628672 100644 --- a/drawBot/context/baseContext.py +++ b/drawBot/context/baseContext.py @@ -1905,7 +1905,19 @@ def listNamedInstances(self, fontNameOrPath: SomePath | None = None) -> dict[str """ if fontNameOrPath is None: fontNameOrPath = self._font - descriptors = getFontDescriptorsFromPath(fontNameOrPath) + if not os.path.exists(fontNameOrPath): + # the font is installed + nsFont = AppKit.NSFont.fontWithName_size_(fontNameOrPath, 10) + # get the url from the font descriptor + url = CoreText.CTFontDescriptorCopyAttribute(nsFont.fontDescriptor(), CoreText.kCTFontURLAttribute) + if url is None: + return dict() + else: + path = url.path() + else: + path = fontNameOrPath + + descriptors = getFontDescriptorsFromPath(path) return variation.getNamedInstancesForDescriptors(descriptors) def tabs(self, tab: tuple[float, str] | None, *tabs: tuple[float, str]): @@ -3013,13 +3025,16 @@ def _getNSFontFromNameOrPath(fontNameOrPath, fontSize, fontNumber): @memoize def getFontDescriptorsFromPath(fontPath): - url = AppKit.NSURL.fileURLWithPath_(fontPath) - assert url is not None - modTime = os.stat(fontPath).st_mtime + if os.path.exists(fontPath): + modTime = os.stat(fontPath).st_mtime + else: + modTime = -1 prevModTime, descriptors = _reloadedFontDescriptors.get(fontPath, (modTime, None)) if modTime == prevModTime: if not descriptors: # Load font from disk, letting the OS handle caching and loading + url = AppKit.NSURL.fileURLWithPath_(fontPath) + assert url is not None descriptors = CoreText.CTFontManagerCreateFontDescriptorsFromURL(url) # Nothing was reloaded, this is the general case: do not cache the # descriptors globally (they are cached per newDrawing session via @@ -3032,7 +3047,11 @@ def getFontDescriptorsFromPath(fontPath): data = AppKit.NSData.dataWithContentsOfFile_(fontPath) descriptors = CoreText.CTFontManagerCreateFontDescriptorsFromData(data) _reloadedFontDescriptors[fontPath] = modTime, descriptors - return tuple(descriptors) + if descriptors is None: + descriptors = tuple() + else: + descriptors = tuple(descriptors) + return descriptors def getFontName(font) -> str | None: From 938ccbfb3f828b0016e73782dd323674b6d437d0 Mon Sep 17 00:00:00 2001 From: Frederik Berlaen Date: Tue, 20 May 2025 13:55:29 +0200 Subject: [PATCH 3/5] not necessary to drop the fontNumber argument --- drawBot/context/baseContext.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drawBot/context/baseContext.py b/drawBot/context/baseContext.py index 6f628672..f097095b 100644 --- a/drawBot/context/baseContext.py +++ b/drawBot/context/baseContext.py @@ -1894,7 +1894,7 @@ def fontNamedInstance(self, name: str, fontNameOrPath: str | SomePath | None = N fontName = self._font raise DrawBotError(f"Can not find instance with name: '{name}' for '{fontName}'.") - def listNamedInstances(self, fontNameOrPath: SomePath | None = None) -> dict[str, dict]: + def listNamedInstances(self, fontNameOrPath: SomePath | None = None, fontNumber: int = 0) -> dict[str, dict]: """ List all named instances from a variable font for the current font. @@ -1907,9 +1907,9 @@ def listNamedInstances(self, fontNameOrPath: SomePath | None = None) -> dict[str fontNameOrPath = self._font if not os.path.exists(fontNameOrPath): # the font is installed - nsFont = AppKit.NSFont.fontWithName_size_(fontNameOrPath, 10) + font = getNSFontFromNameOrPath(fontNameOrPath, 10, fontNumber) # get the url from the font descriptor - url = CoreText.CTFontDescriptorCopyAttribute(nsFont.fontDescriptor(), CoreText.kCTFontURLAttribute) + url = CoreText.CTFontDescriptorCopyAttribute(font.fontDescriptor(), CoreText.kCTFontURLAttribute) if url is None: return dict() else: From 058d3917323ca030e0413b6400e306fba2b46e0c Mon Sep 17 00:00:00 2001 From: Frederik Berlaen Date: Tue, 20 May 2025 14:35:20 +0200 Subject: [PATCH 4/5] remove None test --- drawBot/context/baseContext.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/drawBot/context/baseContext.py b/drawBot/context/baseContext.py index 1c64f099..7f279b02 100644 --- a/drawBot/context/baseContext.py +++ b/drawBot/context/baseContext.py @@ -3029,7 +3029,7 @@ def getFontDescriptorsFromPath(fontPath): modTime = os.stat(fontPath).st_mtime else: modTime = -1 - prevModTime, descriptors = _reloadedFontDescriptors.get(fontPath, (modTime, None)) + prevModTime, descriptors = _reloadedFontDescriptors.get(fontPath, (modTime, tuple())) if modTime == prevModTime: if not descriptors: # Load font from disk, letting the OS handle caching and loading @@ -3047,11 +3047,7 @@ def getFontDescriptorsFromPath(fontPath): data = AppKit.NSData.dataWithContentsOfFile_(fontPath) descriptors = CoreText.CTFontManagerCreateFontDescriptorsFromData(data) _reloadedFontDescriptors[fontPath] = modTime, descriptors - if descriptors is None: - descriptors = tuple() - else: - descriptors = tuple(descriptors) - return descriptors + return tuple(descriptors) def getFontName(font) -> str | None: From fe95ae7460f0cb35e9af7af0ec38da2f8c918023 Mon Sep 17 00:00:00 2001 From: Frederik Berlaen Date: Tue, 20 May 2025 14:48:02 +0200 Subject: [PATCH 5/5] code aesthetics --- drawBot/context/baseContext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drawBot/context/baseContext.py b/drawBot/context/baseContext.py index 7f279b02..6a2f759e 100644 --- a/drawBot/context/baseContext.py +++ b/drawBot/context/baseContext.py @@ -3029,7 +3029,7 @@ def getFontDescriptorsFromPath(fontPath): modTime = os.stat(fontPath).st_mtime else: modTime = -1 - prevModTime, descriptors = _reloadedFontDescriptors.get(fontPath, (modTime, tuple())) + prevModTime, descriptors = _reloadedFontDescriptors.get(fontPath, (modTime, ())) if modTime == prevModTime: if not descriptors: # Load font from disk, letting the OS handle caching and loading