Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/server/window/windowicon_source.py : 52%
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
9import os
10import threading
11from io import BytesIO
12from PIL import Image
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
19log = Logger("icon")
21ARGB_ICONS = envbool("XPRA_ARGB_ICONS", True)
22PNG_ICONS = envbool("XPRA_PNG_ICONS", True)
23DEFAULT_ICONS = envbool("XPRA_DEFAULT_ICONS", True)
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)
30"""
31Mixin for handling the sending of window icon pixels.
32"""
33class WindowIconSource:
35 fallback_window_icon = False
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
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)
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)
68 def cleanup(self):
69 self.cancel_window_icon_timer()
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)
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 }
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
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)
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)
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)
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)
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)
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