From 00762dc7f302c28bcb32d2d91fb087627c38a263 Mon Sep 17 00:00:00 2001 From: Alex Hughes Date: Fri, 28 Jun 2024 13:47:09 -0700 Subject: [PATCH] Install any custom fonts provided with the Job Our adaptor will now install any font files uploaded with the job as a part of its "Launch After Effects" step. It is installing these fonts to the User and not the System so Admin permissions should not be required. Signed-off-by: Alex Hughes --- src/deadline/ae_adaptor/AEAdaptor/adaptor.py | 46 +++++++ .../ae_adaptor/AEAdaptor/font_installer.py | 119 ++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100755 src/deadline/ae_adaptor/AEAdaptor/font_installer.py diff --git a/src/deadline/ae_adaptor/AEAdaptor/adaptor.py b/src/deadline/ae_adaptor/AEAdaptor/adaptor.py index c55d556..846bab4 100644 --- a/src/deadline/ae_adaptor/AEAdaptor/adaptor.py +++ b/src/deadline/ae_adaptor/AEAdaptor/adaptor.py @@ -64,6 +64,8 @@ class AEAdaptor(Adaptor[AdaptorConfiguration]): _exc_info: Exception | None = None _performing_cleanup = False + FONT_EXTENSIONS = {".OTF", "TTF"} + @property def integration_data_interface_version(self) -> SemanticVersion: return SemanticVersion(major=0, minor=1) @@ -285,6 +287,47 @@ def _populate_action_queue(self) -> None: if action_name in self.init_data: self._action_queue.enqueue_action(self._action_from_action_item(action_name)) + def _find_fonts(self): + fonts = set() + for path in os.listdir(os.getcwd()): + if path.startswith("assetroot-"): + asset_dir = os.path.join(os.getcwd(), path) + for asset_path in os.listdir(asset_dir): + _, ext = os.path.splitext(os.path.join(asset_dir, asset_path)) + if ext.upper() in self.FONT_EXTENSIONS: + fonts.add(os.path.join(asset_dir, asset_path)) + return fonts + + def _install_fonts(self): + fonts = self._find_fonts() + installed_fonts = set() + if not fonts: + return + + from . import font_installer + + for font in fonts: + _logger.info("Installing font: %s" % font) + installed, msg = font_installer.install_font(font) + if not installed: + _logger.error(" Error installing font: %s" % msg) + else: + installed_fonts.add(font) + return installed_fonts + + def _remove_fonts(self): + fonts = self._find_fonts() + if not fonts: + return + + from . import font_installer + + for font in fonts: + _logger.info("Uninstalling font: %s" % font) + uninstalled, msg = font_installer.uninstall_font(font) + if not uninstalled: + _logger.error(" Error uninstalling font: %s" % msg) + def on_start(self) -> None: """ For job stickiness. Will start everything required for the Task. @@ -296,6 +339,7 @@ def on_start(self) -> None: - TimeoutError: If After Effects did not complete initialization actions due to timing out. - FileNotFoundError: If the ae_client.py file could not be found. """ + self._install_fonts() # Validate init data against schema cur_dir = os.path.dirname(__file__) schema_dir = os.path.join(cur_dir, "schemas") @@ -385,6 +429,8 @@ def on_cleanup(self): if self._server_thread.is_alive(): _logger.error("Failed to shutdown the After Effects Adaptor server.") + self._remove_fonts() + self._performing_cleanup = False def on_cancel(self): diff --git a/src/deadline/ae_adaptor/AEAdaptor/font_installer.py b/src/deadline/ae_adaptor/AEAdaptor/font_installer.py new file mode 100755 index 0000000..9af4c52 --- /dev/null +++ b/src/deadline/ae_adaptor/AEAdaptor/font_installer.py @@ -0,0 +1,119 @@ +import os +import shutil +import ctypes +from ctypes import wintypes + +try: + import winreg +except ImportError: + import _winreg as winreg + +user32 = ctypes.WinDLL("user32", use_last_error=True) +gdi32 = ctypes.WinDLL("gdi32", use_last_error=True) + +FONTS_REG_PATH = r"Software\Microsoft\Windows NT\CurrentVersion\Fonts" + +HWND_BROADCAST = 0xFFFF +SMTO_ABORTIFHUNG = 0x0002 +WM_FONTCHANGE = 0x001D +GFRI_DESCRIPTION = 1 +GFRI_ISTRUETYPE = 3 + +INSTALL_SCOPE_USER = "USER" +INSTALL_SCOPE_SYSTEM = "SYSTEM" + +FONT_LOCATION_SYSTEM = os.path.join(os.environ.get("SystemRoot"), "Fonts") +FONT_LOCATION_USER = os.path.join(os.environ.get("LocalAppData"), "Microsoft", "Windows", "Fonts") + + +def install_font(src_path, scope=INSTALL_SCOPE_USER): + try: + # copy the font to the Windows Fonts folder + if scope == INSTALL_SCOPE_SYSTEM: + dst_path = os.path.join(FONT_LOCATION_SYSTEM, os.path.basename(src_path)) + registry_scope = winreg.HKEY_LOCAL_MACHINE + else: + dst_path = os.path.join(FONT_LOCATION_USER, os.path.basename(src_path)) + registry_scope = winreg.HKEY_CURRENT_USER + + shutil.copy(src_path, dst_path) + # load the font in the current session + if not gdi32.AddFontResourceW(dst_path): + os.remove(dst_path) + raise WindowsError('AddFontResource failed to load "%s"' % src_path) + # notify running programs + user32.SendMessageTimeoutW( + HWND_BROADCAST, WM_FONTCHANGE, 0, 0, SMTO_ABORTIFHUNG, 1000, None + ) + # store the fontname/filename in the registry + filename = os.path.basename(dst_path) + fontname = os.path.splitext(filename)[0] + # try to get the font's real name + cb = wintypes.DWORD() + if gdi32.GetFontResourceInfoW(filename, ctypes.byref(cb), None, GFRI_DESCRIPTION): + buf = (ctypes.c_wchar * cb.value)() + if gdi32.GetFontResourceInfoW(filename, ctypes.byref(cb), buf, GFRI_DESCRIPTION): + fontname = buf.value + is_truetype = wintypes.BOOL() + cb.value = ctypes.sizeof(is_truetype) + gdi32.GetFontResourceInfoW( + filename, ctypes.byref(cb), ctypes.byref(is_truetype), GFRI_ISTRUETYPE + ) + if is_truetype: + fontname += " (TrueType)" + with winreg.OpenKey(registry_scope, FONTS_REG_PATH, 0, winreg.KEY_SET_VALUE) as key: + winreg.SetValueEx(key, fontname, 0, winreg.REG_SZ, filename) + except Exception: + import traceback + + return False, traceback.format_exc() + return True, "" + + +def uninstall_font(src_path, scope=INSTALL_SCOPE_USER): + try: + # copy the font to the Windows Fonts folder + if scope == INSTALL_SCOPE_SYSTEM: + dst_path = os.path.join(FONT_LOCATION_SYSTEM, os.path.basename(src_path)) + registry_scope = winreg.HKEY_LOCAL_MACHINE + else: + dst_path = os.path.join(FONT_LOCATION_USER, os.path.basename(src_path)) + registry_scope = winreg.HKEY_CURRENT_USER + + # remove the fontname/filename from the registry + filename = os.path.basename(dst_path) + fontname = os.path.splitext(filename)[0] + # try to get the font's real name + cb = wintypes.DWORD() + if gdi32.GetFontResourceInfoW(filename, ctypes.byref(cb), None, GFRI_DESCRIPTION): + buf = (ctypes.c_wchar * cb.value)() + if gdi32.GetFontResourceInfoW(filename, ctypes.byref(cb), buf, GFRI_DESCRIPTION): + fontname = buf.value + is_truetype = wintypes.BOOL() + cb.value = ctypes.sizeof(is_truetype) + gdi32.GetFontResourceInfoW( + filename, ctypes.byref(cb), ctypes.byref(is_truetype), GFRI_ISTRUETYPE + ) + if is_truetype: + fontname += " (TrueType)" + + with winreg.OpenKey(registry_scope, FONTS_REG_PATH, 0, winreg.KEY_SET_VALUE) as key: + winreg.DeleteValue(key, fontname) + + # unload the font in the current session + if not gdi32.RemoveFontResourceW(dst_path): + os.remove(dst_path) + raise WindowsError('RemoveFontResourceW failed to load "%s"' % src_path) + + if os.path.exists(dst_path): + os.remove(dst_path) + + # notify running programs + user32.SendMessageTimeoutW( + HWND_BROADCAST, WM_FONTCHANGE, 0, 0, SMTO_ABORTIFHUNG, 1000, None + ) + except Exception: + import traceback + + return False, traceback.format_exc() + return True, ""