Skip to content

Commit

Permalink
Implement implicit reloading of fonts if they're changed on disk. Fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
justvanrossum committed Mar 24, 2021
1 parent 01afcfa commit 6597601
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 3 deletions.
33 changes: 30 additions & 3 deletions drawBot/context/baseContext.py
Expand Up @@ -2639,11 +2639,38 @@ def getNSFontFromNameOrPath(fontNameOrPath, fontSize, fontNumber):
return CoreText.CTFontCreateWithFontDescriptor(descriptors[fontNumber], fontSize, None)


# Cache for font descriptors that have been reloaded after a font file
# changed on disk. We don't clear this cache, as the number of reloaded
# fonts should generably be within reasonable limits, and re-reloading
# upon every run (think Variable Sliders) is expensive.
# NOTE: It's possible to turn this into a Least Recently Used cache with
# a maximum size, using Python 3.7's insertion order preserving dict
# behavior, but it may not be worth the effort.
_reloadedFontDescriptors = {}


@memoize
def getFontDescriptorsFromPath(fontPath):
url = AppKit.NSURL.fileURLWithPath_(os.path.abspath(fontPath))
assert url is not None
return CoreText.CTFontManagerCreateFontDescriptorsFromURL(url)
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
# @memoize), only store the modification time.
_reloadedFontDescriptors[fontPath] = modTime, None
else:
# The font file was changed on disk since we last used it. We now load
# it from data explicitly, bypassing any OS cache, ensuring we will see
# the updated font.
data = AppKit.NSData.dataWithContentsOfFile_(fontPath)
descriptors = CoreText.CTFontManagerCreateFontDescriptorsFromData(data)
_reloadedFontDescriptors[fontPath] = modTime, descriptors
return descriptors


def getFontName(font):
Expand Down
25 changes: 25 additions & 0 deletions tests/testMisc.py
@@ -1,8 +1,11 @@
import sys
import os
import pathlib
import unittest
import io
import tempfile
from collections import OrderedDict
from fontTools.ttLib import TTFont
import drawBot
from drawBot.misc import DrawBotError, warnings
from drawBot.scriptTools import ScriptRunner
Expand Down Expand Up @@ -302,6 +305,28 @@ def test_textBoxCharacterBounds(self):
self.assertEqual([i.baselineOffset for i in bounds], [2.109375, 6.328125, 7.5, 10.0])
self.assertEqual([str(i.formattedSubString) for i in bounds], ['hello hello ', 'foo foo ', 'bar bar ', 'world world '])

def test_reloadFont(self):
src = pathlib.Path(__file__).resolve().parent / "data" / "MutatorSans.ttf"
assert src.exists()
with tempfile.NamedTemporaryFile(suffix=".ttf") as ff:
ff.write(src.read_bytes())
firstModTime = os.stat(ff.name).st_mtime
drawBot.newDrawing()
path = drawBot.BezierPath()
path.text("E", font=ff.name, fontSize=1000)
self.assertEqual((60.0, 0.0, 340.0, 700.0), path.bounds())
ff.seek(0)
ttf = TTFont(ff)
ttf["glyf"]["E"].coordinates[0] = (400, 800)
ff.seek(0)
ttf.save(ff)
secondModTime = os.stat(ff.name).st_mtime
assert firstModTime != secondModTime, (firstModTime, secondModTime)
drawBot.newDrawing() # to clear the memioze cache in baseContext
path = drawBot.BezierPath()
path.text("E", font=ff.name, fontSize=1000)
self.assertEqual((60.0, 0.0, 400.0, 800.0), path.bounds())


def _roundInstanceLocations(instanceLocations):
return {instanceName: {tag: round(value, 3) for tag, value in location.items()} for instanceName, location in instanceLocations.items()}
Expand Down

0 comments on commit 6597601

Please sign in to comment.