Skip to content

Commit

Permalink
preliminary support for transparent windows using the gtk2 Pixmap bac…
Browse files Browse the repository at this point in the history
…king client:

* windows expose an "has-alpha" metadata attribute so we know the backing needs the alpha channel - store this property in the metadata cache since it cannot be changed (can it?)
* if enabled, the client also sets the window colormap to "rgba"
* assume rgb32 is always available for now: this needs to be tested for differently for client and server, and even for different types of clients / different types of window backings
* shadow servers disable alpha
* use get_best_encoding to select a suitable encoding for handling transparency (tested only with "png" and raw "rgb32")
* handle transparency in screenshots too
* change "contents-handle" to return a PixmapWrapper object which can get pixel data from the X11 server by calling XGetImage
* extract pixels from the XImage using PIL

git-svn-id: https://xpra.org/svn/Xpra/trunk@3368 3bb7dfac-3a0b-4e04-842a-767bc560f471
  • Loading branch information
totaam committed May 12, 2013
1 parent b7bf912 commit 7c824f4
Show file tree
Hide file tree
Showing 17 changed files with 272 additions and 85 deletions.
19 changes: 15 additions & 4 deletions src/xpra/client/client_window_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,15 @@ def init_window(self, metadata):
self._been_mapped = False
self._override_redirect_windows = []
self.update_metadata(metadata)

def setup_window(self):
self.new_backing(*self._size)


def make_new_backing(self, backing_class, w, h):
w = max(1, w)
h = max(1, h)
has_alpha = self._metadata.get("has-alpha", False)
lock = None
backing = self._backing
if backing:
Expand All @@ -79,13 +80,16 @@ def make_new_backing(self, backing_class, w, h):
if lock:
lock.acquire()
if backing is None:
bc = backing_class
if USE_FAKE_BACKING:
from xpra.client.fake_window_backing import FakeBacking
backing_class = FakeBacking
backing = backing_class(self._id, w, h)
bc = FakeBacking
self.debug("make_new_backing(%s, %s, %s) effective backing class=%s", backing_class, w, h, bc)
backing = bc(self._id, w, h, has_alpha)
if self._client.mmap_enabled:
backing.enable_mmap(self._client.mmap)
backing.init(w, h)
self.debug("make_new_backing(%s, %s, %s) init with alpha=%s", backing_class, w, h, has_alpha)
backing.init(w, h, has_alpha)
finally:
if lock:
lock.release()
Expand Down Expand Up @@ -218,6 +222,10 @@ def metadata_replace(match):
xid = self._metadata.get("xid")
self.set_xid(xid)

if "has-alpha" in self._metadata:
has_alpha = self._metadata.get("has-alpha")
self.set_alpha(has_alpha)

def set_window_type(self, window_types):
self.debug("set_window_type(%s)", window_types)
hints = 0
Expand All @@ -228,6 +236,9 @@ def set_window_type(self, window_types):
self.debug("setting window type to %s - %s", window_type, hint)
self.set_type_hint(hint)

def set_alpha(self, has_alpha):
pass

def set_xid(self, xid):
pass

Expand Down
10 changes: 10 additions & 0 deletions src/xpra/client/gtk2/client_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ def init_window(self, metadata):
gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
GTKClientWindowBase.init_window(self, metadata)

def set_alpha(self, has_alpha):
self.debug("set_alpha(%s)", has_alpha)
if has_alpha:
screen = self.get_screen()
rgba = screen.get_rgba_colormap()
if rgba is None:
self.error("cannot handle window transparency!")
else:
self.set_colormap(rgba)

def new_backing(self, w, h):
self._backing = self.make_new_backing(BACKING_CLASS, w, h)

Expand Down
30 changes: 24 additions & 6 deletions src/xpra/client/gtk2/pixmap_backing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
# later version. See the file COPYING for details.

import gtk
from gtk import gdk
import cairo

Expand All @@ -17,13 +18,23 @@
"""
class PixmapBacking(GTK2WindowBacking):

def __init__(self, wid, w, h):
GTK2WindowBacking.__init__(self, wid, w, h)
def __init__(self, wid, w, h, has_alpha):
GTK2WindowBacking.__init__(self, wid, w, h, has_alpha)

def init(self, w, h):
def init(self, w, h, has_alpha):
old_backing = self._backing
assert w<32768 and h<32768, "dimensions too big: %sx%s" % (w, h)
self._backing = gdk.Pixmap(gdk.get_default_root_window(), w, h)
self._has_alpha = has_alpha
if has_alpha:
self._backing = gdk.Pixmap(None, w, h, 32)
screen = self._backing.get_screen()
rgba = screen.get_rgba_colormap()
if rgba is not None:
self._backing.set_colormap(rgba)
else:
self._has_alpha = False
if not self._has_alpha:
self._backing = gdk.Pixmap(gdk.get_default_root_window(), w, h)
cr = self._backing.cairo_create()
cr.set_source_rgb(1, 1, 1)
if old_backing is not None:
Expand Down Expand Up @@ -55,6 +66,13 @@ def _do_paint_rgb24(self, img_data, x, y, width, height, rowstride, options, cal
return True

def _do_paint_rgb32(self, img_data, x, y, width, height, rowstride, options, callbacks):
gc = self._backing.new_gc()
self._backing.draw_rgb_32_image(gc, x, y, width, height, gdk.RGB_DITHER_NONE, img_data, rowstride)
log.debug("do_paint_rgb32(%s bytes, %s, %s, %s, %s, %s, %s, %s)", len(img_data), x, y, width, height, rowstride, options, callbacks)
#log.info("data head=%s", [hex(ord(v))[2:] for v in list(img_data[:500])])
pixbuf = gdk.pixbuf_new_from_data(img_data, gtk.gdk.COLORSPACE_RGB, True, 8, width, height, rowstride)
log.debug("do_paint_rgb32(..) backing depth=%s", self._backing.get_depth())
cr = self._backing.cairo_create()
cr.rectangle(x, y, width, height)
cr.set_source_pixbuf(pixbuf, x, y)
cr.set_operator(cairo.OPERATOR_SOURCE)
cr.paint()
return True
4 changes: 2 additions & 2 deletions src/xpra/client/gtk2/window_backing.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@
"""
class GTK2WindowBacking(GTKWindowBacking):

def __init__(self, wid, w, h):
def __init__(self, wid, w, h, has_alpha):
GTKWindowBacking.__init__(self, wid)

def init(self, w, h):
def init(self, w, h, has_alpha):
raise Exception("override me!")


Expand Down
4 changes: 2 additions & 2 deletions src/xpra/client/gtk3/client_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ def init_window(self):
gtk.Window.__init__(self)
GTKClientWindowBase.init_window(self)

def new_backing(self, w, h):
self._backing = self.make_new_backing(CairoBacking, w, h)
def new_backing(self, w, h, has_alpha):
self._backing = self.make_new_backing(CairoBacking, w, h, has_alpha)


def xget_u32_property(self, target, name):
Expand Down
4 changes: 2 additions & 2 deletions src/xpra/client/gtk_base/cairo_backing.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
This is a complete waste of CPU! Please complain to pycairo.
"""
class CairoBacking(GTKWindowBacking):
def __init__(self, wid, w, h):
def __init__(self, wid, w, h, has_alpha):
GTKWindowBacking.__init__(self, wid)

def init(self, w, h):
def init(self, w, h, has_alpha):
old_backing = self._backing
#should we honour self.depth here?
self._backing = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h)
Expand Down
4 changes: 2 additions & 2 deletions src/xpra/client/qt4/pixmap_backing.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
"""
class QtPixmapBacking(WindowBackingBase):

def __init__(self, wid, w, h):
def __init__(self, wid, w, h, has_alpha):
WindowBackingBase.__init__(self, wid, getQtScheduler().idle_add)

def init(self, w, h):
def init(self, w, h, has_alpha):
#TODO: repaint from old backing!
#old_backing = self._backing
assert w<32768 and h<32768, "dimensions too big: %sx%s" % (w, h)
Expand Down
21 changes: 15 additions & 6 deletions src/xpra/client/window_backing_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class WindowBackingBase(object):
def __init__(self, wid, idle_add):
self.wid = wid
self.idle_add = idle_add
self._has_alpha = False
self._backing = None
self._last_pixmap_data = None
self._video_use_swscale = True
Expand Down Expand Up @@ -118,16 +119,24 @@ def process_delta(self, raw_data, width, height, rowstride, options):

def paint_image(self, coding, img_data, x, y, width, height, rowstride, options, callbacks):
""" can be called from any thread """
#log("paint_image(%s, %s bytes, %s, %s, %s, %s, %s, %s, %s)", coding, len(img_data), x, y, width, height, rowstride, options, callbacks)
assert coding in ENCODINGS, "encoding %s is not supported!" % coding
assert has_PIL
buf = BytesIOClass(img_data)
img = Image.open(buf)
assert img.mode=="RGB", "invalid image mode: %s" % img.mode
raw_data = img.tostring("raw", "RGB")
#PIL flattens the data to a continuous straightforward RGB format:
rowstride = width*3
img_data = self.process_delta(raw_data, width, height, rowstride, options)
self.idle_add(self.do_paint_rgb24, img_data, x, y, width, height, rowstride, options, callbacks)
assert img.mode in ("RGB", "RGBA"), "invalid image mode: %s" % img.mode
raw_data = img.tostring("raw", img.mode)
if img.mode=="RGB":
#PIL flattens the data to a continuous straightforward RGB format:
if rowstride<=0:
rowstride = width*3
img_data = self.process_delta(raw_data, width, height, rowstride, options)
self.idle_add(self.do_paint_rgb24, img_data, x, y, width, height, rowstride, options, callbacks)
elif img.mode=="RGBA":
if rowstride<=0:
rowstride = width*4
img_data = self.process_delta(raw_data, width, height, rowstride, options)
self.idle_add(self.do_paint_rgb32, img_data, x, y, width, height, rowstride, options, callbacks)
return False

def paint_webp(self, img_data, x, y, width, height, rowstride, options, callbacks):
Expand Down
2 changes: 1 addition & 1 deletion src/xpra/gtk_common/pixbuf_to_rgb.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ def get_rgb_rawdata(pixmap, x, y, width, height, logger=None):
int(1000*(time.time()-start)), colormap.get_visual().depth)
raw_data = pixbuf.get_pixels()
rowstride = pixbuf.get_rowstride()
return (x, y, width, height, raw_data, rowstride)
return (x, y, width, height, raw_data, "RGB", rowstride)
2 changes: 1 addition & 1 deletion src/xpra/scripts/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def warn(msg):
_has_PIL = False

#if you use gtk3, you *must* have PIL installed so we can handle rgb24...
ENCODINGS = ["rgb24"]
ENCODINGS = ["rgb24", "rgb32"]
if _has_PIL:
ENCODINGS.append("png")
ENCODINGS.append("jpeg")
Expand Down
5 changes: 4 additions & 1 deletion src/xpra/server/shadow_server_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ def is_tray(self):
def is_OR(self):
return False

def has_alpha(self):
return False

def acknowledge_changes(self):
pass

Expand Down Expand Up @@ -145,7 +148,7 @@ def send_windows_and_cursors(self, ss):
window = self._id_to_window[wid]
assert window == self.root_window_model
w, h = self.root.get_size()
props = ("title", "client-machine", "size-hints", "window-type")
props = ("title", "client-machine", "size-hints", "window-type", "has-alpha")
ss.new_window("new-window", wid, window, 0, 0, w, h, props, self.client_properties.get(ss.uuid))
#ss.send_cursor(self.cursor_data)

Expand Down
5 changes: 2 additions & 3 deletions src/xpra/server/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,13 +466,12 @@ def set_screen_sizes(self, screen_sizes):
def _make_metadata(self, wid, window, propname):
cache = self.window_metdata_cache.setdefault(wid, {})
if len(cache)==0:
#these never change and are never queried,
#but we want to report on them via "xpra info",
#so populate them here
#these never change so populate them here just once
cache["override-redirect"] = window.is_OR()
cache["tray"] = window.is_tray()
if get_xwindow:
cache["xid"] = hex(get_xwindow(window.client_window))
cache["has-alpha"] = window.has_alpha()
if propname in cache:
return cache.copy()
props = self.do_make_metadata(window, propname)
Expand Down
38 changes: 23 additions & 15 deletions src/xpra/server/window_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,9 +490,9 @@ def send_full_screen_update():
self.process_damage_region(damage_time, window, x, y, w, h, actual_encoding, options)

def get_best_encoding(self, batching, window, pixel_count, ww, wh, current_encoding):
return self.do_get_best_encoding(batching, window.is_tray(), window.is_OR(), pixel_count, ww, wh, current_encoding)
return self.do_get_best_encoding(batching, window.has_alpha(), window.is_tray(), window.is_OR(), pixel_count, ww, wh, current_encoding)

def do_get_best_encoding(self, batching, is_tray, is_OR, pixel_count, ww, wh, current_encoding):
def do_get_best_encoding(self, batching, has_alpha, is_tray, is_OR, pixel_count, ww, wh, current_encoding):
"""
decide whether we send a full screen update
using the video encoder or if a small lossless region(s) is a better choice
Expand All @@ -501,6 +501,10 @@ def switch():
coding = self.find_common_lossless_encoder(current_encoding, ww*wh)
debug("temporarily switching to %s encoder for %s pixels", coding, pixel_count)
return coding
if has_alpha and current_encoding not in ("png", "rgb32"):
for x in ("png", "rgb32"):
if x in ENCODINGS:
return x
if is_tray:
#tray needs a lossless encoder
return switch()
Expand Down Expand Up @@ -583,9 +587,9 @@ def do_process_damage_region(self, damage_time, window, x, y, w, h, coding, opti
return
if self.is_cancelled(sequence):
return
px, py, pw, ph, rgb_data, rowstride = rgb
px, py, pw, ph, rgb_data, rgb_format, rowstride = rgb
process_damage_time = time.time()
data = (damage_time, process_damage_time, self.wid, px, py, pw, ph, coding, rgb_data, rowstride, sequence, options)
data = (damage_time, process_damage_time, self.wid, px, py, pw, ph, coding, rgb_data, rgb_format, rowstride, sequence, options)
self._sequence += 1
debug("process_damage_regions: adding pixel data %s to queue, elapsed time: %.1f ms", data[:6], 1000*(time.time()-damage_time))
def make_data_packet_cb(*args):
Expand Down Expand Up @@ -710,7 +714,7 @@ def damage_packet_acked(self, damage_packet_sequence, width, height, decode_time
if self._damage_delayed is not None and self._damage_delayed_expired:
gobject.idle_add(self.may_send_delayed)

def make_data_packet(self, damage_time, process_damage_time, wid, x, y, w, h, coding, rgbdata, rowstride, sequence, options):
def make_data_packet(self, damage_time, process_damage_time, wid, x, y, w, h, coding, rgbdata, rgb_format, rowstride, sequence, options):
"""
Picture encoding - non-UI thread.
Converts a damage item picked from the 'damage_data_queue'
Expand All @@ -730,7 +734,7 @@ def make_data_packet(self, damage_time, process_damage_time, wid, x, y, w, h, co
assert rgbdata, "data is missing"
debug("make_data_packet: damage data: %s", (wid, x, y, w, h, coding))
start = time.time()
if self._mmap and self._mmap_size>0 and len(rgbdata)>256:
if self._mmap and self._mmap_size>0 and len(rgbdata)>256 and rgb_format=="RGB":
#try with mmap (will change coding to "mmap" if it succeeds)
coding, data = self.mmap_send(coding, rgbdata)
else:
Expand All @@ -744,8 +748,12 @@ def make_data_packet(self, damage_time, process_damage_time, wid, x, y, w, h, co
delta = lsequence
data = xor_str(rgbdata, ldata)

if rgb_format.upper()!="RGB" and rgb_format.upper()!="RGBX":
assert rgb_format.upper()=="RGBA", "invalid rgb format: %s" % rgb_format
assert coding in ("rgb32", "png", ), "invalid encoding for %s: %s" % (rgb_format, coding)

if coding in ("jpeg", "png"):
data, client_options = self.PIL_encode(w, h, coding, data, rowstride, options)
data, client_options = self.PIL_encode(w, h, coding, data, rgb_format, rowstride, options)
elif coding=="x264":
#x264 needs sizes divisible by 2:
w = w & 0xFFFE
Expand All @@ -754,8 +762,8 @@ def make_data_packet(self, damage_time, process_damage_time, wid, x, y, w, h, co
data, client_options = self.video_encode(wid, x, y, w, h, coding, data, rowstride, options)
elif coding=="vpx":
data, client_options = self.video_encode(wid, x, y, w, h, coding, data, rowstride, options)
elif coding=="rgb24":
data, client_options = self.rgb24_encode(data)
elif coding=="rgb24" or coding=="rgb32":
data, client_options = self.rgb_encode(coding, data)
elif coding=="webp":
data, client_options = self.webp_encode(w, h, data, rowstride, options)
elif coding=="mmap":
Expand Down Expand Up @@ -798,19 +806,19 @@ def webp_encode(self, w, h, data, rowstride, options):
q = min(99, max(1, q))
return Compressed("webp", str(EncodeRGB(image, quality=q).data)), {"quality" : q}

def rgb24_encode(self, data):
def rgb_encode(self, coding, data):
#compress here and return a wrapper so network code knows it is already zlib compressed:
zlib = zlib_compress("rgb24", data)
zlib = zlib_compress(coding, data)
if not self.encoding_client_options or not self.supports_rgb24zlib:
return zlib, {}
#wrap it using "Compressed" so the network layer receiving it
#won't decompress it (leave it to the client's draw thread)
return Compressed("rgb24", zlib.data), {"zlib" : zlib.level}
return Compressed(coding, zlib.data), {"zlib" : zlib.level}

def PIL_encode(self, w, h, coding, data, rowstride, options):
def PIL_encode(self, w, h, coding, data, rgb_format, rowstride, options):
assert coding in ENCODINGS
import Image
im = Image.fromstring("RGB", (w, h), data, "raw", "RGB", rowstride)
im = Image.fromstring(rgb_format, (w, h), data, "raw", rgb_format, rowstride)
buf = StringIOClass()
client_options = {}
if coding=="jpeg":
Expand All @@ -823,7 +831,7 @@ def PIL_encode(self, w, h, coding, data, rowstride, options):
client_options["quality"] = q
else:
assert coding=="png"
debug("sending as %s", coding)
debug("sending as %s, mode=%s", coding, im.mode)
#transparency = False
#transparency=transparency
im.save(buf, coding.upper())
Expand Down
6 changes: 1 addition & 5 deletions src/xpra/x11/gtk_x11/composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,7 @@ def set_pixmap():
return self._contents_handle

def do_get_property_contents(self, name):
handle = self.get_property("contents-handle")
if handle is None:
return None
else:
return handle.pixmap
return self.get_property("contents-handle")

def do_xpra_unmap_event(self, *args):
self.invalidate_pixmap()
Expand Down
Loading

0 comments on commit 7c824f4

Please sign in to comment.