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# This file is part of Xpra. 

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

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

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

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

6 

7import cairo 

8from cairo import ( #pylint: disable=no-name-in-module 

9 Context, ImageSurface, 

10 FORMAT_ARGB32, FORMAT_RGB30, FORMAT_RGB24, FORMAT_RGB16_565, 

11 OPERATOR_SOURCE, OPERATOR_CLEAR, OPERATOR_OVER, 

12 ) 

13from gi.repository import GLib, Gdk 

14 

15from xpra.client.paint_colors import get_paint_box_color 

16from xpra.client.window_backing_base import WindowBackingBase, fire_paint_callbacks, SCROLL_ENCODING 

17from xpra.os_util import memoryview_to_bytes, monotonic_time 

18from xpra.util import envbool 

19from xpra.log import Logger 

20 

21log = Logger("paint", "cairo") 

22 

23FORMATS = {-1 : "INVALID"} 

24for attr in dir(cairo): 

25 if attr.startswith("FORMAT_"): 

26 FORMATS[getattr(cairo, attr)] = attr.replace("FORMAT_", "") 

27 

28 

29def cairo_paint_pointer_overlay(context, cursor_data, px : int, py : int, start_time): 

30 if not cursor_data: 

31 return 

32 elapsed = max(0, monotonic_time()-start_time) 

33 if elapsed>6: 

34 return 

35 from xpra.gtk_common.gtk_util import get_pixbuf_from_data 

36 from xpra.codecs.argb.argb import unpremultiply_argb #@UnresolvedImport 

37 cw = cursor_data[3] 

38 ch = cursor_data[4] 

39 xhot = cursor_data[5] 

40 yhot = cursor_data[6] 

41 pixels = cursor_data[8] 

42 x = px-xhot 

43 y = py-yhot 

44 alpha = max(0, (5.0-elapsed)/5.0) 

45 log("cairo_paint_pointer_overlay%s drawing pointer with cairo, alpha=%s", 

46 (context, x, y, start_time), alpha) 

47 context.translate(x, y) 

48 context.rectangle(0, 0, cw, ch) 

49 argb = unpremultiply_argb(pixels) 

50 img_data = memoryview_to_bytes(argb) 

51 pixbuf = get_pixbuf_from_data(img_data, True, cw, ch, cw*4) 

52 context.set_operator(OPERATOR_OVER) 

53 Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0) 

54 context.paint() 

55 

56 

57class CairoBackingBase(WindowBackingBase): 

58 

59 HAS_ALPHA = envbool("XPRA_ALPHA", True) 

60 

61 def __init__(self, wid, window_alpha, _pixel_depth=0): 

62 super().__init__(wid, window_alpha and self.HAS_ALPHA) 

63 self.idle_add = GLib.idle_add 

64 self.size = 0, 0 

65 self.render_size = 0, 0 

66 

67 def init(self, ww : int, wh : int, bw : int, bh : int): 

68 mod = self.size!=(bw, bh) or self.render_size!=(ww, wh) 

69 self.size = bw, bh 

70 self.render_size = ww, wh 

71 if mod: 

72 self.create_surface() 

73 

74 def get_info(self): 

75 info = super().get_info() 

76 info.update({ 

77 "type" : "Cairo", 

78 "rgb-formats" : self.RGB_MODES, 

79 }) 

80 return info 

81 

82 

83 def create_surface(self): 

84 bw, bh = self.size 

85 old_backing = self._backing 

86 #should we honour self.depth here? 

87 self._backing = None 

88 if bw==0 or bh==0: 

89 #this can happen during cleanup 

90 return None 

91 self._backing = ImageSurface(FORMAT_ARGB32, bw, bh) 

92 cr = Context(self._backing) 

93 cr.set_operator(OPERATOR_CLEAR) 

94 cr.set_source_rgba(1, 1, 1, 1) 

95 cr.rectangle(0, 0, bw, bh) 

96 cr.fill() 

97 if old_backing is not None: 

98 oldw, oldh = old_backing.get_width(), old_backing.get_height() 

99 sx, sy, dx, dy, w, h = self.gravity_copy_coords(oldw, oldh, bw, bh) 

100 cr.translate(dx-sx, dy-sy) 

101 cr.rectangle(sx, sy, w, h) 

102 cr.fill() 

103 cr.set_operator(OPERATOR_SOURCE) 

104 cr.set_source_surface(old_backing, 0, 0) 

105 cr.paint() 

106 self._backing.flush() 

107 return cr 

108 

109 def close(self): 

110 if self._backing: 

111 self._backing.finish() 

112 WindowBackingBase.close(self) 

113 

114 

115 def cairo_paint_pixbuf(self, pixbuf, x : int, y : int, options): 

116 """ must be called from UI thread """ 

117 log("source pixbuf: %s", pixbuf) 

118 w, h = pixbuf.get_width(), pixbuf.get_height() 

119 self.cairo_paint_from_source(Gdk.cairo_set_source_pixbuf, pixbuf, x, y, w, h, w, h, options) 

120 

121 def cairo_paint_surface(self, img_surface, x : int, y : int, width : int, height : int, options): 

122 iw, ih = img_surface.get_width(), img_surface.get_height() 

123 log("source image surface: %s", 

124 (img_surface.get_format(), iw, ih, img_surface.get_stride(), img_surface.get_content(), )) 

125 def set_source_surface(gc, surface, sx, sy): 

126 gc.set_source_surface(surface, sx, sy) 

127 self.cairo_paint_from_source(set_source_surface, img_surface, x, y, iw, ih, width, height, options) 

128 

129 def cairo_paint_from_source(self, set_source_fn, source, 

130 x : int, y : int, iw : int, ih : int, width : int, height : int, options): 

131 """ must be called from UI thread """ 

132 log("cairo_paint_surface%s backing=%s, paint box line width=%i", 

133 (set_source_fn, source, x, y, iw, ih, width, height, options), 

134 self._backing, self.paint_box_line_width) 

135 gc = Context(self._backing) 

136 if self.paint_box_line_width: 

137 gc.save() 

138 

139 gc.rectangle(x, y, width, height) 

140 gc.clip() 

141 

142 gc.set_operator(OPERATOR_CLEAR) 

143 gc.rectangle(x, y, width, height) 

144 gc.fill() 

145 

146 gc.set_operator(OPERATOR_SOURCE) 

147 gc.translate(x, y) 

148 if iw!=width or ih!=height: 

149 gc.scale(width/iw, height/ih) 

150 gc.rectangle(0, 0, width, height) 

151 set_source_fn(gc, source, 0, 0) 

152 gc.paint() 

153 

154 if self.paint_box_line_width: 

155 gc.restore() 

156 encoding = options.get("encoding") 

157 self.cairo_paint_box(gc, encoding, x, y, width, height) 

158 

159 def cairo_paint_box(self, gc, encoding, x, y, w, h): 

160 color = get_paint_box_color(encoding) 

161 gc.set_line_width(self.paint_box_line_width) 

162 gc.set_source_rgba(*color) 

163 gc.rectangle(x, y, w, h) 

164 gc.stroke() 

165 

166 def _do_paint_rgb16(self, img_data, x, y, width, height, render_width, render_height, rowstride, options): 

167 return self._do_paint_rgb(FORMAT_RGB16_565, False, img_data, 

168 x, y, width, height, render_width, render_height, rowstride, options) 

169 

170 def _do_paint_rgb24(self, img_data, x : int, y : int, width : int, height : int, 

171 render_width : int, render_height : int, rowstride : int, options): 

172 return self._do_paint_rgb(FORMAT_RGB24, False, img_data, 

173 x, y, width, height, render_width, render_height, rowstride, options) 

174 

175 def _do_paint_rgb30(self, img_data, x, y, width, height, render_width, render_height, rowstride, options): 

176 return self._do_paint_rgb(FORMAT_RGB30, True, img_data, 

177 x, y, width, height, render_width, render_height, rowstride, options) 

178 

179 def _do_paint_rgb32(self, img_data, x : int, y : int, width : int, height : int, 

180 render_width : int, render_height : int, rowstride : int, options): 

181 if self._alpha_enabled: 

182 cformat = FORMAT_ARGB32 

183 else: 

184 cformat = FORMAT_RGB24 

185 return self._do_paint_rgb(cformat, True, img_data, 

186 x, y, width, height, render_width, render_height, rowstride, options) 

187 

188 def _do_paint_rgb(self, *args): 

189 raise NotImplementedError() 

190 

191 

192 def get_encoding_properties(self): 

193 props = WindowBackingBase.get_encoding_properties(self) 

194 if SCROLL_ENCODING: 

195 props["encoding.scrolling"] = True 

196 return props 

197 

198 

199 def paint_scroll(self, img_data, options, callbacks): 

200 self.idle_add(self.do_paint_scroll, img_data, callbacks) 

201 

202 def do_paint_scroll(self, scrolls, callbacks): 

203 old_backing = self._backing 

204 gc = self.create_surface() 

205 if not gc: 

206 fire_paint_callbacks(callbacks, False, message="no context") 

207 return 

208 gc.set_operator(OPERATOR_SOURCE) 

209 for sx,sy,sw,sh,xdelta,ydelta in scrolls: 

210 gc.set_source_surface(old_backing, xdelta, ydelta) 

211 x = sx+xdelta 

212 y = sy+ydelta 

213 gc.rectangle(x, y, sw, sh) 

214 gc.fill() 

215 if self.paint_box_line_width>0: 

216 self.cairo_paint_box(gc, "scroll", x, y, sw, sh) 

217 del gc 

218 self._backing.flush() 

219 fire_paint_callbacks(callbacks) 

220 

221 

222 def nasty_rgb_via_png_paint(self, cairo_format, has_alpha : bool, img_data, 

223 x : int, y : int, width : int, height : int, rowstride : int, rgb_format): 

224 log.warn("nasty_rgb_via_png_paint%s", 

225 (cairo_format, has_alpha, len(img_data), x, y, width, height, rowstride, rgb_format)) 

226 #PIL fallback 

227 from PIL import Image 

228 if has_alpha: 

229 oformat = "RGBA" 

230 else: 

231 oformat = "RGB" 

232 #use frombytes rather than frombuffer to be compatible with python3 new-style buffers 

233 #this is slower, but since this codepath is already dreadfully slow, we don't care 

234 bdata = memoryview_to_bytes(img_data) 

235 src_format = rgb_format.replace("X", "A") 

236 try: 

237 img = Image.frombytes(oformat, (width,height), bdata, "raw", src_format, rowstride, 1) 

238 except ValueError as e: 

239 log("PIL Image frombytes:", exc_info=True) 

240 raise Exception("failed to parse raw %s data as %s to %s: %s" % ( 

241 rgb_format, src_format, oformat, e)) from None 

242 #This is insane, the code below should work, but it doesn't: 

243 # img_data = bytearray(img.tostring('raw', oformat, 0, 1)) 

244 # pixbuf = new_from_data(img_data, COLORSPACE_RGB, True, 8, width, height, rowstride) 

245 # success = self.cairo_paint_pixbuf(pixbuf, x, y) 

246 #So we still rountrip via PNG: 

247 from io import BytesIO 

248 png = BytesIO() 

249 img.save(png, format="PNG") 

250 reader = BytesIO(png.getvalue()) 

251 png.close() 

252 img = ImageSurface.create_from_png(reader) 

253 self.cairo_paint_surface(img, x, y, width, height, {}) 

254 return True 

255 

256 

257 def cairo_draw(self, context): 

258 log("cairo_draw: size=%s, render-size=%s, offsets=%s, pointer_overlay=%s", 

259 self.size, self.render_size, self.offsets, self.pointer_overlay) 

260 if self._backing is None: 

261 return 

262 #try: 

263 # log("clip rectangles=%s", context.copy_clip_rectangle_list()) 

264 #except: 

265 # log.error("clip:", exc_info=True) 

266 ww, wh = self.render_size 

267 w, h = self.size 

268 if ww==0 or w==0 or wh==0 or h==0: 

269 return 

270 if w!=ww or h!=wh: 

271 context.scale(ww/w, wh/h) 

272 x, y = self.offsets[:2] 

273 if x!=0 or y!=0: 

274 context.translate(x, y) 

275 context.set_operator(OPERATOR_SOURCE) 

276 context.set_source_surface(self._backing, 0, 0) 

277 context.paint() 

278 if self.pointer_overlay and self.cursor_data: 

279 px, py, _size, start_time = self.pointer_overlay[2:] 

280 spx = round(w*px/ww) 

281 spy = round(h*py/wh) 

282 cairo_paint_pointer_overlay(context, self.cursor_data, x+spx, y+spy, start_time)