Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2# This file is part of Xpra. 

3# Copyright (C) 2010-2019 Antoine Martin <antoine@xpra.org> 

4# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com> 

5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any 

6# later version. See the file COPYING for details. 

7#pylint: disable-msg=E1101 

8 

9import os 

10import threading 

11from io import BytesIO 

12from PIL import Image 

13 

14from xpra.os_util import monotonic_time, load_binary_file, memoryview_to_bytes 

15from xpra.net import compression 

16from xpra.util import envbool, envint, csv 

17from xpra.log import Logger 

18 

19log = Logger("icon") 

20 

21ARGB_ICONS = envbool("XPRA_ARGB_ICONS", True) 

22PNG_ICONS = envbool("XPRA_PNG_ICONS", True) 

23DEFAULT_ICONS = envbool("XPRA_DEFAULT_ICONS", True) 

24 

25LOG_THEME_DEFAULT_ICONS = envbool("XPRA_LOG_THEME_DEFAULT_ICONS", False) 

26SAVE_WINDOW_ICONS = envbool("XPRA_SAVE_WINDOW_ICONS", False) 

27MAX_ARGB_PIXELS = envint("XPRA_MAX_ARGB_PIXELS", 1024) 

28 

29 

30""" 

31Mixin for handling the sending of window icon pixels. 

32""" 

33class WindowIconSource: 

34 

35 fallback_window_icon = False 

36 

37 def __init__(self, window_icon_encodings, icons_encoding_options): 

38 self.window_icon_encodings = window_icon_encodings 

39 self.icons_encoding_options = icons_encoding_options #icon caps 

40 

41 self.has_png = PNG_ICONS and ("png" in self.window_icon_encodings) 

42 self.has_default = DEFAULT_ICONS and ("default" in self.window_icon_encodings) 

43 log("WindowIconSource(%s, %s) has_png=%s", 

44 window_icon_encodings, icons_encoding_options, self.has_png) 

45 

46 self.window_icon_data = None 

47 self.send_window_icon_timer = 0 

48 self.theme_default_icons = icons_encoding_options.strtupleget("default.icons") 

49 self.window_icon_greedy = icons_encoding_options.boolget("greedy", False) 

50 self.window_icon_size = icons_encoding_options.intpair("size", (64, 64)) 

51 self.window_icon_max_size = icons_encoding_options.intpair("max_size", self.window_icon_size) 

52 self.window_icon_max_size = ( 

53 max(self.window_icon_max_size[0], 16), 

54 max(self.window_icon_max_size[1], 16), 

55 ) 

56 self.window_icon_size = ( 

57 min(self.window_icon_size[0], self.window_icon_max_size[0]), 

58 min(self.window_icon_size[1], self.window_icon_max_size[1]), 

59 ) 

60 self.window_icon_size = ( 

61 max(self.window_icon_size[0], 16), 

62 max(self.window_icon_size[1], 16), 

63 ) 

64 log("client icon settings: size=%s, max_size=%s", self.window_icon_size, self.window_icon_max_size) 

65 if LOG_THEME_DEFAULT_ICONS: 

66 log("theme_default_icons=%s", self.theme_default_icons) 

67 

68 def cleanup(self): 

69 self.cancel_window_icon_timer() 

70 

71 def cancel_window_icon_timer(self): 

72 swit = self.send_window_icon_timer 

73 if swit: 

74 self.send_window_icon_timer = 0 

75 self.source_remove(swit) 

76 

77 def get_info(self) -> dict: 

78 idata = self.window_icon_data 

79 if not idata: 

80 return {} 

81 w, h, fmt, data = idata 

82 return { 

83 "icon" : { 

84 "width" : w, 

85 "height" : h, 

86 "format" : fmt, 

87 "bytes" : len(data), 

88 } 

89 } 

90 

91 @staticmethod 

92 def get_fallback_window_icon(): 

93 if WindowIconSource.fallback_window_icon is False: 

94 try: 

95 from xpra.platform.paths import get_icon_filename 

96 icon_filename = get_icon_filename("xpra.png") 

97 log("get_fallback_window_icon() icon filename=%s", icon_filename) 

98 assert os.path.exists(icon_filename), "xpra icon not found: %s" % icon_filename 

99 img = Image.open(icon_filename) 

100 icon_data = load_binary_file(icon_filename) 

101 icon = (img.size[0], img.size[1], "png", icon_data) 

102 WindowIconSource.fallback_window_icon = icon 

103 return icon 

104 except Exception as e: 

105 log.warn("failed to get fallback icon: %s", e) 

106 WindowIconSource.fallback_window_icon = False 

107 return WindowIconSource.fallback_window_icon 

108 

109 def send_window_icon(self): 

110 #some of this code could be moved to the work queue half, meh 

111 assert self.ui_thread == threading.current_thread() 

112 if self.suspended: 

113 return 

114 #this runs in the UI thread 

115 icons = self.window.get_property("icons") 

116 log("send_window_icon window %s found %i icons", self.window, len(icons or ())) 

117 if not icons: 

118 #this is a bit dirty: 

119 #we figure out if the client is likely to have an icon for this wmclass already, 

120 #(assuming the window even has a 'class-instance'), and if not we send the default 

121 try: 

122 c_i = self.window.get_property("class-instance") 

123 except Exception: 

124 c_i = None 

125 if c_i and len(c_i)==2: 

126 wm_class = c_i[0].encode("utf-8") 

127 if wm_class in self.theme_default_icons: 

128 log("%s in client theme icons already (not sending default icon)", self.theme_default_icons) 

129 return 

130 #try to load the icon for this class-instance from the theme: 

131 icons = [] 

132 done = set() 

133 for sizes in (self.window_icon_size, self.window_icon_max_size, (48, 64)): 

134 for size in sizes: 

135 if size in done: 

136 continue 

137 done.add(size) 

138 icon = self.window.get_default_window_icon(size) 

139 if icon: 

140 icons.append(icon) 

141 log("send_window_icon window %s using default window icon", self.window) 

142 max_w, max_h = self.window_icon_max_size 

143 icon = self.choose_icon(icons, max_w, max_h) 

144 if not icon: 

145 #try again, without size restrictions: 

146 #(we'll downscale it) 

147 icon = self.choose_icon(icons) 

148 if not icon: 

149 if not self.window_icon_greedy: 

150 return 

151 #"greedy": client does not set a default icon, so we must provide one every time 

152 #to make sure that the window icon does get set to something 

153 #(our icon is at least better than the window manager's default) 

154 if self.has_default: 

155 #client will set the default itself, 

156 #send a mostly empty packet: 

157 packet = ("window-icon", self.wid, 0, 0, "default", "") 

158 log("queuing window icon update: %s", packet) 

159 #this is cheap, so don't use the encode thread, 

160 #and make sure we don't send another one via the timer: 

161 self.cancel_window_icon_timer() 

162 self.queue_packet(packet, wait_for_more=True) 

163 return 

164 icon = WindowIconSource.get_fallback_window_icon() 

165 log("using fallback window icon") 

166 if not icon: 

167 log("no suitable icon") 

168 return 

169 self.window_icon_data = icon 

170 if not self.send_window_icon_timer: 

171 #call compress via the work queue 

172 #and delay sending it by a bit to allow basic icon batching: 

173 w, h = self.window_icon_data[:2] 

174 delay = min(1000, max(50, w*h*self.batch_config.delay_per_megapixel//1000000)) 

175 log("send_window_icon() window=%s, wid=%s, compression scheduled in %sms for batch delay=%i", 

176 self.window, self.wid, delay, self.batch_config.delay_per_megapixel) 

177 self.send_window_icon_timer = self.timeout_add(delay, self.call_in_encode_thread, 

178 True, self.compress_and_send_window_icon) 

179 

180 def compress_and_send_window_icon(self): 

181 #this runs in the work queue 

182 self.send_window_icon_timer = 0 

183 idata = self.window_icon_data 

184 if not idata or not self.has_png: 

185 return 

186 w, h, pixel_format, pixel_data = idata 

187 log("compress_and_send_window_icon() %ix%i in %s format, %i bytes for wid=%i", 

188 w, h, pixel_format, len(pixel_data), self.wid) 

189 assert pixel_format in ("BGRA", "RGBA", "png"), "invalid window icon format %s" % pixel_format 

190 if pixel_format=="BGRA": 

191 #BGRA data is always unpremultiplied 

192 #(that's what we get from NetWMIcons) 

193 from xpra.codecs.argb.argb import premultiply_argb #@UnresolvedImport 

194 pixel_data = premultiply_argb(pixel_data) 

195 

196 max_w, max_h = self.window_icon_max_size 

197 #use png if supported and if "premult_argb32" is not supported by the client (ie: html5) 

198 #or if we must downscale it (bigger than what the client is willing to deal with), 

199 #or if we want to save window icons 

200 must_scale = w>max_w or h>max_h 

201 log("compress_and_send_window_icon: %sx%s (max-size=%s, standard-size=%s), pixel_format=%s", 

202 w, h, self.window_icon_max_size, self.window_icon_size, pixel_format) 

203 must_convert = pixel_format!="png" 

204 log(" must convert=%s, must scale=%s", must_convert, must_scale) 

205 

206 image = None 

207 if must_scale or must_convert or SAVE_WINDOW_ICONS: 

208 #we're going to need a PIL Image: 

209 if pixel_format=="png": 

210 image = Image.open(BytesIO(pixel_data)) 

211 else: 

212 image = Image.frombuffer("RGBA", (w,h), memoryview_to_bytes(pixel_data), "raw", pixel_format, 0, 1) 

213 if must_scale: 

214 #scale the icon down to the size the client wants 

215 #(we should scale + paste to preserve the aspect ratio, meh) 

216 icon_w, icon_h = self.window_icon_size 

217 if float(w)/icon_w>=float(h)/icon_h: 

218 rh = min(max_h, h*icon_w//w) 

219 rw = icon_w 

220 else: 

221 rw = min(max_w, w*icon_h//h) 

222 rh = icon_h 

223 log("scaling window icon down to %sx%s", rw, rh) 

224 image = image.resize((rw, rh), Image.ANTIALIAS) 

225 if SAVE_WINDOW_ICONS: 

226 filename = "server-window-%i-icon-%i.png" % (self.wid, int(monotonic_time())) 

227 image.save(filename, 'PNG') 

228 log("server window icon saved to %s", filename) 

229 

230 if image: 

231 #image got converted or scaled, get the new pixel data: 

232 output = BytesIO() 

233 image.save(output, "png") 

234 pixel_data = output.getvalue() 

235 output.close() 

236 w, h = image.size 

237 wrapper = compression.Compressed("png", pixel_data) 

238 packet = ("window-icon", self.wid, w, h, wrapper.datatype, wrapper) 

239 log("queuing window icon update: %s", packet) 

240 self.queue_packet(packet, wait_for_more=True) 

241 

242 

243 def choose_icon(self, icons, max_w=1024, max_h=1024): 

244 if not icons: 

245 return None 

246 log("choose_icon from: %s", csv("%ix%i %s" % icon[:3] for icon in icons)) 

247 size_image = dict((icon[0]*icon[1], icon) for icon in icons if icon[0]<max_w and icon[1]<max_h) 

248 if not size_image: 

249 return None 

250 #we should choose one whose size is close to what the client wants, 

251 #take the biggest one for now: 

252 largest_size = sorted(size_image)[-1] 

253 icon = size_image[largest_size] 

254 log("choose_icon(..)=%ix%i %s", *icon[:3]) 

255 return icon