1import ctypes 2import os 3import platform 4import plistlib 5 6from shutil import copy2, rmtree 7from subprocess import call, check_output 8 9HERE = os.path.dirname(__file__) 10SYSTEM = platform.system().lower() 11 12 13class FontInstaller(object): 14 def __init__(self, logger, font_dir=None, **fonts): 15 self.logger = logger 16 self.font_dir = font_dir 17 self.installed_fonts = False 18 self.created_dir = False 19 self.fonts = fonts 20 21 def __call__(self, env_options=None, env_config=None): 22 return self 23 24 def __enter__(self): 25 for _, font_path in self.fonts.items(): 26 font_name = font_path.split('/')[-1] 27 install = getattr(self, 'install_%s_font' % SYSTEM, None) 28 if not install: 29 self.logger.warning('Font installation not supported on %s' % SYSTEM) 30 return False 31 if install(font_name, font_path): 32 self.installed_fonts = True 33 self.logger.info('Installed font: %s' % font_name) 34 else: 35 self.logger.warning('Unable to install font: %s' % font_name) 36 37 def __exit__(self, exc_type, exc_val, exc_tb): 38 if not self.installed_fonts: 39 return False 40 41 for _, font_path in self.fonts.items(): 42 font_name = font_path.split('/')[-1] 43 remove = getattr(self, 'remove_%s_font' % SYSTEM, None) 44 if not remove: 45 self.logger.warning('Font removal not supported on %s' % SYSTEM) 46 return False 47 if remove(font_name, font_path): 48 self.logger.info('Removed font: %s' % font_name) 49 else: 50 self.logger.warning('Unable to remove font: %s' % font_name) 51 52 def install_linux_font(self, font_name, font_path): 53 if not self.font_dir: 54 self.font_dir = os.path.join(os.path.expanduser('~'), '.fonts') 55 if not os.path.exists(self.font_dir): 56 os.makedirs(self.font_dir) 57 self.created_dir = True 58 if not os.path.exists(os.path.join(self.font_dir, font_name)): 59 copy2(font_path, self.font_dir) 60 try: 61 fc_cache_returncode = call('fc-cache') 62 return not fc_cache_returncode 63 except OSError: # If fontconfig doesn't exist, return False 64 self.logger.error('fontconfig not available on this Linux system.') 65 return False 66 67 def install_darwin_font(self, font_name, font_path): 68 if not self.font_dir: 69 self.font_dir = os.path.join(os.path.expanduser('~'), 70 'Library/Fonts') 71 if not os.path.exists(self.font_dir): 72 os.makedirs(self.font_dir) 73 self.created_dir = True 74 installed_font_path = os.path.join(self.font_dir, font_name) 75 if not os.path.exists(installed_font_path): 76 copy2(font_path, self.font_dir) 77 78 # Per https://github.com/web-platform-tests/results-collection/issues/218 79 # installing Ahem on macOS is flaky, so check if it actually installed 80 with open(os.devnull, 'w') as f: 81 fonts = check_output(['/usr/sbin/system_profiler', '-xml', 'SPFontsDataType'], stderr=f) 82 83 try: 84 # if py3 85 load_plist = plistlib.loads 86 except AttributeError: 87 load_plist = plistlib.readPlistFromString 88 fonts = load_plist(fonts) 89 assert len(fonts) == 1 90 for font in fonts[0]['_items']: 91 if font['path'] == installed_font_path: 92 return True 93 return False 94 95 def install_windows_font(self, _, font_path): 96 hwnd_broadcast = 0xFFFF 97 wm_fontchange = 0x001D 98 99 gdi32 = ctypes.WinDLL('gdi32') 100 if gdi32.AddFontResourceW(font_path): 101 from ctypes import wintypes 102 wparam = 0 103 lparam = 0 104 SendNotifyMessageW = ctypes.windll.user32.SendNotifyMessageW 105 SendNotifyMessageW.argtypes = [wintypes.HANDLE, wintypes.UINT, 106 wintypes.WPARAM, wintypes.LPARAM] 107 return bool(SendNotifyMessageW(hwnd_broadcast, wm_fontchange, 108 wparam, lparam)) 109 110 def remove_linux_font(self, font_name, _): 111 if self.created_dir: 112 rmtree(self.font_dir) 113 else: 114 os.remove('%s/%s' % (self.font_dir, font_name)) 115 try: 116 fc_cache_returncode = call('fc-cache') 117 return not fc_cache_returncode 118 except OSError: # If fontconfig doesn't exist, return False 119 self.logger.error('fontconfig not available on this Linux system.') 120 return False 121 122 def remove_darwin_font(self, font_name, _): 123 if self.created_dir: 124 rmtree(self.font_dir) 125 else: 126 os.remove(os.path.join(self.font_dir, font_name)) 127 return True 128 129 def remove_windows_font(self, _, font_path): 130 hwnd_broadcast = 0xFFFF 131 wm_fontchange = 0x001D 132 133 gdi32 = ctypes.WinDLL('gdi32') 134 if gdi32.RemoveFontResourceW(font_path): 135 from ctypes import wintypes 136 wparam = 0 137 lparam = 0 138 SendNotifyMessageW = ctypes.windll.user32.SendNotifyMessageW 139 SendNotifyMessageW.argtypes = [wintypes.HANDLE, wintypes.UINT, 140 wintypes.WPARAM, wintypes.LPARAM] 141 return bool(SendNotifyMessageW(hwnd_broadcast, wm_fontchange, 142 wparam, lparam)) 143