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