From 5c8672560b347c913f1795fccb6abbf2882ff9b2 Mon Sep 17 00:00:00 2001 From: Alex Hughes Date: Tue, 25 Jun 2024 17:02:37 -0700 Subject: [PATCH] Add and implement a new "Font Installer" system This font installer system will package up any non standard fonts used in the After Effects scene and upload them as job attachments in the "Input Files". The adaptor now will install any fonts uploaded with the job as a part of its Launch After Effects step. Signed-off-by: Alex Hughes --- src/deadline/ae_adaptor/AEAdaptor/adaptor.py | 44 ++++++ .../ae_adaptor/AEAdaptor/font_installer.py | 129 ++++++++++++++++++ src/deadline/ae_submitter/data/InitData.jsx | 72 ++++++++++ 3 files changed, 245 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..742f9c7 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,45 @@ 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 +337,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 +427,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..1b937a0 --- /dev/null +++ b/src/deadline/ae_adaptor/AEAdaptor/font_installer.py @@ -0,0 +1,129 @@ +import os +import shutil +import ctypes +from ctypes import wintypes +import sys +import ntpath +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, "" diff --git a/src/deadline/ae_submitter/data/InitData.jsx b/src/deadline/ae_submitter/data/InitData.jsx index 7fa11fe..093015a 100644 --- a/src/deadline/ae_submitter/data/InitData.jsx +++ b/src/deadline/ae_submitter/data/InitData.jsx @@ -87,9 +87,81 @@ function __generateInitData() } } } + var fontReferences = generateFontReferences(); + logger.debug("Get Fonts: " + key, scriptFileInitDataName); + for (var i = 0; i < fontReferences.length; i++) { + detectedItemsList.push(fontReferences[i]); + } dcProperties.jobAttachments.autoDetectedInputFiles.set(detectedItemsList); } + function _generateFontReferences() { + var fontLocations = []; + var usedList = app.project.usedFonts; + for (var i = 0; i < usedList.length; i++) { + var font = usedList[i].font; + var fontFamilyName = font.familyName; + var familyStyle = font.styleName; + var fontName = fontFamilyName + " " + familyStyle + ".otf"; + fontLocations.push([fontName, fontLocation]); + } + return fontLocations + } + + function _oldGenerateFontReferences() { + var fontLocations = []; + var items = app.project.items; + for (var i = items.length; i >= 1; i--) { + var myItem = app.project.item(i); + if (myItem instanceof CompItem) { + for (var j = myItem.layers.length; j >= 1; j--) { + var myLayer = myItem.layers[j]; + if (myLayer instanceof TextLayer){ + var textDocument = myLayer.text.sourceText.value; + var fontLocation = textDocument.fontLocation; + var fontFamilyName = textDocument.fontFamily; + var familyStyle = textDocument.fontStyle; + var fontName = fontFamilyName + " " + familyStyle + ".otf"; + fontLocations.push([fontName, fontLocation]); + } + } + } + } + return fontLocations + } + + function generateFontReferences() { + var rawFontPaths = []; + if (parseInt(app.version[0] + app.version[1]) >= 24) { + rawFontPaths = _generateFontReferences(); + } else { + rawFontPaths = _oldGenerateFontReferences(); + } + var _tempFilePath = dcUtil.normPath(Folder.temp.fsName + "/" + "tempFonts"); + var formattedFontPaths = []; + var tempFontPath = new Folder(_tempFilePath); + if (!tempFontPath.exists) { + tempFontPath.create(); + } + + + for (var i = 0; i < rawFontPaths.length; i++) { + var fontName = rawFontPaths[i][0]; + var fontLocation = rawFontPaths[i][1]; + if (fontLocation.indexOf("C:\\Windows\\Fonts") !== -1) { + // We are filtering out fonts installed in C:\Windows\Fonts potentially here. + // I would make the argument that if a font is installed to the system (not an Adobe Font), that + // it should be managed on a system level not through the submitter. + continue + } + var fontFile = File(fontLocation); + var _filePath = dcUtil.normPath(_tempFilePath + "/" + fontName); + fontFile.copy(_filePath); + formattedFontPaths.push(_filePath); + } + return formattedFontPaths; + } + function initAutoDetectOutputDirectories() { var detectedOutputDirectories= [];