Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/gtk_base/cairo_backing_base.py : 54%
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.
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
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
21log = Logger("paint", "cairo")
23FORMATS = {-1 : "INVALID"}
24for attr in dir(cairo):
25 if attr.startswith("FORMAT_"):
26 FORMATS[getattr(cairo, attr)] = attr.replace("FORMAT_", "")
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()
57class CairoBackingBase(WindowBackingBase):
59 HAS_ALPHA = envbool("XPRA_ALPHA", True)
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
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()
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
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
109 def close(self):
110 if self._backing:
111 self._backing.finish()
112 WindowBackingBase.close(self)
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)
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)
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()
139 gc.rectangle(x, y, width, height)
140 gc.clip()
142 gc.set_operator(OPERATOR_CLEAR)
143 gc.rectangle(x, y, width, height)
144 gc.fill()
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()
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)
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()
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)
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)
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)
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)
188 def _do_paint_rgb(self, *args):
189 raise NotImplementedError()
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
199 def paint_scroll(self, img_data, options, callbacks):
200 self.idle_add(self.do_paint_scroll, img_data, callbacks)
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)
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
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)