Skip to content

Commit

Permalink
#90: split cairo code so we can add the partial cython workarounds fo…
Browse files Browse the repository at this point in the history
…r gtk3

git-svn-id: https://xpra.org/svn/Xpra/trunk@7557 3bb7dfac-3a0b-4e04-842a-767bc560f471
  • Loading branch information
totaam committed Sep 11, 2014
1 parent 9534f2a commit a32f656
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 87 deletions.
5 changes: 5 additions & 0 deletions src/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1577,6 +1577,11 @@ def cython_add(*args, **kwargs):
["xpra/x11/gtk3_x11/gdk_display_source.pyx"],
**pkgconfig("gtk+-3.0")
))
#cairo workaround:
cython_add(Extension("xpra.client.gtk3.cairo_workaround",
["xpra/client/gtk3/cairo_workaround.pyx", buffers_c],
**pkgconfig("pycairo")
))
else:
#below uses gtk/gdk:
cython_add(Extension("xpra.x11.gtk_x11.gdk_display_source",
Expand Down
1 change: 1 addition & 0 deletions src/xpra/client/client_window_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def init_window(self, metadata):

def new_backing(self, w, h):
backing_class = self.get_backing_class()
log("new_backing(%s, %s) backing_class=%s", w, h, backing_class)
assert backing_class is not None
self._backing = self.make_new_backing(backing_class, w, h)
self._backing.border = self.border
Expand Down
58 changes: 58 additions & 0 deletions src/xpra/client/gtk2/cairo_backing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# This file is part of Xpra.
# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com>
# Copyright (C) 2012-2014 Antoine Martin <antoine@devloop.org.uk>
# 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 cairo

from xpra.client.gtk_base.cairo_backing_base import CairoBackingBase
from xpra.gtk_common.gtk_util import pixbuf_new_from_data, COLORSPACE_RGB
from xpra.os_util import builtins
_memoryview = builtins.__dict__.get("memoryview")

from xpra.log import Logger
log = Logger("paint", "cairo")


"""
With python2 / gtk2, we can create an ImageSurface using either:
* cairo.ImageSurface.create_for_data
* pixbuf_new_from_data
Failing that, we use the horrible roundtrip via PNG using PIL.
"""
class CairoBacking(CairoBackingBase):

#with gtk2 we can convert these directly to a cairo image surface:
RGB_MODES = ["ARGB", "XRGB", "RGBA", "RGBX", "RGB"]


def __repr__(self):
return "gtk2.CairoBacking(%s)" % self._backing


def _do_paint_rgb(self, cairo_format, has_alpha, img_data, x, y, width, height, rowstride, options):
""" must be called from UI thread """
log("cairo._do_paint_rgb(%s, %s, %s bytes,%s,%s,%s,%s,%s,%s)", cairo_format, has_alpha, len(img_data), x, y, width, height, rowstride, options)
rgb_format = options.strget("rgb_format", "RGB")
if _memoryview and isinstance(img_data, _memoryview):
#Pixbuf cannot use the memoryview directly:
img_data = img_data.tobytes()

if rgb_format in ("ARGB", "XRGB"):
#the pixel format is also what cairo expects
#maybe we should also check that the stride is acceptable for cairo?
#cairo_stride = cairo.ImageSurface.format_stride_for_width(cairo_format, width)
#log("cairo_stride=%s, stride=%s", cairo_stride, rowstride)
img_surface = cairo.ImageSurface.create_for_data(img_data, cairo_format, width, height, rowstride)
return self.cairo_paint_surface(img_surface, x, y)

if rgb_format in ("RGBA", "RGBX", "RGB"):
#with GTK2, we can use a pixbuf from RGB(A) pixels
if rgb_format=="RGBA":
#we have to unpremultiply for pixbuf!
img_data = self.unpremultiply(img_data)
pixbuf = pixbuf_new_from_data(img_data, COLORSPACE_RGB, has_alpha, 8, width, height, rowstride)
return self.cairo_paint_pixbuf(pixbuf, x, y)

self.nasty_rgb_via_png_paint(cairo_format, has_alpha, img_data, x, y, width, height, rowstride, rgb_format)
2 changes: 1 addition & 1 deletion src/xpra/client/gtk2/client_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

USE_CAIRO = os.environ.get("XPRA_USE_CAIRO_BACKING", "0")=="1"
if USE_CAIRO:
from xpra.client.gtk_base.cairo_backing import CairoBacking
from xpra.client.gtk2.cairo_backing import CairoBacking
BACKING_CLASS = CairoBacking
else:
from xpra.client.gtk2.pixmap_backing import PixmapBacking
Expand Down
88 changes: 88 additions & 0 deletions src/xpra/client/gtk3/cairo_backing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# This file is part of Xpra.
# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com>
# Copyright (C) 2012-2014 Antoine Martin <antoine@devloop.org.uk>
# 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 cairo
from gi.repository import GObject #@UnresolvedImport
from gi.repository import GdkPixbuf #@UnresolvedImport

from xpra.client.gtk_base.cairo_backing_base import CairoBackingBase
from xpra.os_util import BytesIOClass
from xpra.client.gtk_base.gtk_window_backing_base import GTKWindowBacking
from xpra.client.window_backing_base import fire_paint_callbacks
from xpra.os_util import builtins
_memoryview = builtins.__dict__.get("memoryview")

from xpra.client.gtk3.cairo_workaround import set_image_surface_data

from xpra.log import Logger
log = Logger("paint", "cairo")


"""
An area we draw onto with cairo
This must be used with gtk3 since gtk3 no longer supports gdk pixmaps
/RANT: ideally we would want to use pycairo's create_for_data method:
#surf = cairo.ImageSurface.create_for_data(data, cairo.FORMAT_RGB24, width, height)
but this is disabled in most cases, or does not accept our rowstride, so we cannot use it.
Instead we have to use PIL to convert via a PNG or Pixbuf!
"""
class CairoBacking(CairoBackingBase):

RGB_MODES = ["ARGB", "XRGB", "RGBA", "RGBX", "RGB"]


def __repr__(self):
return "gtk3.CairoBacking(%s)" % self._backing


def paint_image(self, coding, img_data, x, y, width, height, options, callbacks):
log("cairo.paint_image(%s, %s bytes,%s,%s,%s,%s,%s,%s) alpha_enabled=%s", coding, len(img_data), x, y, width, height, options, callbacks, self._alpha_enabled)
#catch PNG and jpeg we can handle via cairo or pixbufloader respectively
#(both of which need to run from the UI thread)
if coding.startswith("png") or coding=="jpeg":
def ui_paint_image():
if not self._backing:
fire_paint_callbacks(callbacks, False)
return
try:
if coding.startswith("png"):
reader = BytesIOClass(img_data)
img = cairo.ImageSurface.create_from_png(reader)
success = self.cairo_paint_surface(img, x, y)
else:
assert coding=="jpeg"
pbl = GdkPixbuf.PixbufLoader()
pbl.write(img_data)
pbl.close()
pixbuf = pbl.get_pixbuf()
del pbl
success = self.cairo_paint_pixbuf(pixbuf, x, y)
except:
log.error("cairo error during paint", exc_info=True)
success = False
fire_paint_callbacks(callbacks, success)
GObject.idle_add(ui_paint_image)
return
#this will end up calling do_paint_rgb24 after converting the pixels to RGB
GTKWindowBacking.paint_image(self, coding, img_data, x, y, width, height, options, callbacks)


def _do_paint_rgb(self, cairo_format, has_alpha, img_data, x, y, width, height, rowstride, options):
""" must be called from UI thread """
log("cairo._do_paint_rgb(%s, %s, %s bytes,%s,%s,%s,%s,%s,%s)", cairo_format, has_alpha, len(img_data), x, y, width, height, rowstride, options)
if _memoryview and isinstance(img_data, _memoryview):
#Pixbuf cannot use the memoryview directly:
img_data = img_data.tobytes()

rgb_format = options.strget("rgb_format", "RGB")
#this format we can handle with the workaround:
if format==cairo.FORMAT_RGB24 and rgb_format=="RGB":
img_surface = cairo.ImageSurface(cairo_format, width, height)
set_image_surface_data(img_surface, rgb_format, img_data, width, height, rowstride)
return self.cairo_paint_surface(img_surface, x, y)

self.nasty_rgb_via_png_paint(cairo_format, has_alpha, img_data, x, y, width, height, rowstride, rgb_format)
126 changes: 126 additions & 0 deletions src/xpra/client/gtk3/cairo_workaround.pyx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# This file is part of Xpra.
# Copyright (C) 2014 Antoine Martin <antoine@devloop.org.uk>
# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
# later version. See the file COPYING for details.

# What is this workaround you ask?
# Well, pycairo can't handle raw pixel data in RGB format,
# and that's despite the documentation saying otherwise:
# http://cairographics.org/pythoncairopil/
# Because of this glaring omission, we would have to roundtrip via PNG.
# This workaround populates an image surface with RGB pixels using Cython.
# (not the most efficient implementation, but still 10 times faster than the alternative)
#
#"cairo.ImageSurface.create_for_data" is not implemented in GTK3!
# http://cairographics.org/documentation/pycairo/3/reference/surfaces.html#cairo.ImageSurface.create_for_data
# "Not yet available in Python 3"
#
#It is available in the cffi cairo bindings, which can be used instead of pycairo
# but then we can't use it from the draw callbacks:
# https://mail.gnome.org/archives/python-hackers-list/2011-December/msg00004.html
# "PyGObject just lacks the glue code that allows it to pass the statically-wrapped
# cairo.Pattern to introspected methods"

import cairo


cdef extern from "../../codecs/buffers/buffers.h":
int object_as_buffer(object obj, const void ** buffer, Py_ssize_t * buffer_len)

cdef extern from "Python.h":
ctypedef object PyObject
object PyBuffer_FromMemory(void *ptr, Py_ssize_t size)
int PyObject_AsReadBuffer(object obj, void ** buffer, Py_ssize_t * buffer_len) except -1

#void * PyCObject_Import(char *, char *) except NULL
ctypedef struct PyTypeObject:
pass
ctypedef struct PyObject:
Py_ssize_t ob_refcnt
PyTypeObject *ob_type
void * PyCapsule_Import(const char *name, int no_block)

cdef extern from "cairo/cairo.h":
ctypedef struct cairo_surface_t:
pass

#typedef enum _cairo_format {
ctypedef enum cairo_format_t:
CAIRO_FORMAT_INVALID
CAIRO_FORMAT_ARGB32
CAIRO_FORMAT_RGB24
CAIRO_FORMAT_A8
CAIRO_FORMAT_A1
CAIRO_FORMAT_RGB16_565
CAIRO_FORMAT_RGB30

unsigned char * cairo_image_surface_get_data(cairo_surface_t *surface)
cairo_format_t cairo_image_surface_get_format(cairo_surface_t *surface)

int cairo_image_surface_get_width (cairo_surface_t *surface)
int cairo_image_surface_get_height (cairo_surface_t *surface)
int cairo_image_surface_get_stride (cairo_surface_t *surface)


cdef extern from "pycairo/pycairo.h":
ctypedef struct Pycairo_CAPI_t:
PyTypeObject *ImageSurface_Type
ctypedef struct PycairoSurface:
#PyObject_HEAD
cairo_surface_t *surface
#PyObject *base; /* base object used to create surface, or NULL */
ctypedef PycairoSurface PycairoImageSurface

CAIRO_FORMAT = {
CAIRO_FORMAT_INVALID : "Invalid",
CAIRO_FORMAT_ARGB32 : "ARGB32",
CAIRO_FORMAT_RGB24 : "RGB24",
CAIRO_FORMAT_A8 : "A8",
CAIRO_FORMAT_A1 : "A1",
CAIRO_FORMAT_RGB16_565 : "RGB16_565",
CAIRO_FORMAT_RGB30 : "RGB30",
}

def set_image_surface_data(object image_surface, rgb_format, object pixel_data, int width, int height, int stride):
print("set_image_surface_data%s" % str((image_surface, rgb_format, len(pixel_data), width, height, stride)))
#convert pixel_data to a C buffer:
cdef const unsigned char * cbuf = <unsigned char *> 0
cdef Py_ssize_t cbuf_len = 0
assert object_as_buffer(pixel_data, <const void**> &cbuf, &cbuf_len)==0, "cannot convert %s to a readable buffer" % type(pixel_data)
assert cbuf_len>=height*stride, "pixel buffer is too small for %sx%s with stride=%s: only %s bytes, expected %s" % (width, height, stride, cbuf_len, height*stride)
#convert cairo.ImageSurface python object to a cairo_surface_t
if not isinstance(image_surface, cairo.ImageSurface):
raise TypeError("object %r is not a %r" % (image_surface, cairo.ImageSurface))
cdef cairo_surface_t * surface = (<PycairoImageSurface *> image_surface).surface
cdef unsigned char * data = cairo_image_surface_get_data(surface)
#get surface attributes:
cdef cairo_format_t format = cairo_image_surface_get_format(surface)
cdef int istride = cairo_image_surface_get_stride(surface)
cdef int iwidth = cairo_image_surface_get_width(surface)
cdef int iheight = cairo_image_surface_get_height(surface)
assert iwidth>=width and iheight>=height, "invalid image surface: expected at least %sx%s but got %sx%s" % (width, height, iwidth, iheight)
assert istride>=iwidth*4, "invalid image stride: expected at least %s but got %s" % (iwidth*4, istride)
#just deal with the formats we care about:
if format==CAIRO_FORMAT_RGB24 and rgb_format=="RGB":
for x in range(width):
for y in range(height):
data[x*4 + 0 + y*istride] = cbuf[x*3 + 0 + y*stride] #R
data[x*4 + 1 + y*istride] = cbuf[x*3 + 1 + y*stride] #G
data[x*4 + 2 + y*istride] = cbuf[x*3 + 2 + y*stride] #B
data[x*4 + 3 + y*istride] = 255 #X
return
#note: this one is currently unused because it doesn't work
#and I don't know why
#(we just disable 'rgb32' for gtk3... and fallback to png)
elif format==CAIRO_FORMAT_ARGB32 and rgb_format=="RGBA":
for x in range(width):
for y in range(height):
data[x*4 + 0 + y*istride] = cbuf[x*4 + 3 + y*stride] #A
data[x*4 + 1 + y*istride] = cbuf[x*4 + 0 + y*stride] #R
data[x*4 + 2 + y*istride] = cbuf[x*4 + 1 + y*stride] #G
data[x*4 + 3 + y*istride] = cbuf[x*4 + 2 + y*stride] #B
return
print("ERROR: cairo workaround not implemented for %s / %s" % (rgb_format, CAIRO_FORMAT.get(format, "unknown")))

cdef Pycairo_CAPI_t * Pycairo_CAPI
Pycairo_CAPI = <Pycairo_CAPI_t*> PyCapsule_Import("cairo.CAPI", 0);
2 changes: 2 additions & 0 deletions src/xpra/client/gtk3/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ def do_get_core_encodings(self):
for x in ("webp", ):
if x in cencs:
cencs.remove(x)
#for some reason, the cairo_workaround does not work for ARGB32
#cencs.append("rgb32")
return cencs


Expand Down
2 changes: 1 addition & 1 deletion src/xpra/client/gtk3/client_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from gi.repository import Gdk #@UnresolvedImport @UnusedImport
from gi.repository import GdkPixbuf #@UnresolvedImport @UnusedImport

from xpra.client.gtk_base.cairo_backing import CairoBacking
from xpra.client.gtk3.cairo_backing import CairoBacking
from xpra.client.gtk_base.gtk_client_window_base import GTKClientWindowBase, HAS_X11_BINDINGS
from xpra.log import Logger
log = Logger("gtk", "window")
Expand Down
Loading

0 comments on commit a32f656

Please sign in to comment.