Convert PIL Image to Cairo ImageSurface
Asked Answered
C

2

5

I'm trying to create a cairo ImageSurface from a PIL image, the code I have so far is:

im = Image.open(filename)
imstr = im.tostring()
a = array.array('B', imstr)
height, width = im.size
stride = cairo.ImageSurface.format_stride_for_width(cairo.FORMAT_RGB24, width)
return cairo.ImageSurface.create_for_data(a, cairo.FORMAT_ARGB24, width, height, stride)

But this is giving me

TypeError: buffer is not long enough.

I don't really understand why this is, perhaps I don't understand image formats well enough.

I'm using cairo 1.10.

Cordey answered 30/9, 2011 at 12:11 Comment(4)
may this be of some help ? zetcode.com/tutorials/cairographicstutorial/cairoimagesAshtonashtonunderlyne
The problem is that the image I have is a jpg and cairo only loads from png files.Cordey
If it's only that ! Convert the image with gimp ?Ashtonashtonunderlyne
I mean that the purpose of the program is to work with a single image but the image is not known, all I know about it is that it will be a jpg.Cordey
T
5

Cairo's create_for_data() is wants a writeable buffer object (a string can be used as a buffer object, but it's not writable), and it only supports 32 bits per pixel data (RGBA, or RGB followed by one unused byte). PIL, on the other hand, provides a 24bpp RGB read-only buffer object.

I suggest you tell PIL to add an alpha channel, then convert the PIL buffer to a numpy array to get a writable buffer for Cairo.

im = Image.open(filename)
im.putalpha(256) # create alpha channel
arr = numpy.array(im)
height, width, channels = arr.shape
surface = cairo.ImageSurface.create_for_data(arr, cairo.FORMAT_RGB24, width, height)
Temptation answered 30/9, 2011 at 18:19 Comment(5)
This pretty much works except that the colours are messed up, which I suspect is to do with the ordering of the RGBA pixel values in cairo being dependent on the endianness of the machine I'm on. I've tried just reversing the order of the pixel values in the numpy array but that doesn't seem to work. Any suggestions?Cordey
Managed to make it work by reordering the pixels in the numpy array from RGBA to BGRA.Cordey
There's also a recipe in the Cairo website that explains how to do it: cairographics.org/pythoncairopilEndospore
create_for_data has been removed in recent versions of pycairo so the code above will fail with a "not implemented" error.Arguseyed
@Arguseyed it's present on my Debian Bullseye system, pycairo version 1.16.2.Hereof
H
5

The accepted version doesn't work correctly if:

  • Your image has colors
  • Your image is not opaque
  • Your image is in a mode different from RGB(A)

In cairo image colors have their value premultiplied by the value of alpha, and they are stored as a 32 bit word using the native CPU endianness. That means that the PIL image:

r1 g1 b1 a1 r2 g2 b2 a2 ...

is stored in cairo in a little endian CPU as:

b1*a1 g1*a1 r1*a1 a1 b2*a2 g2*a2 r2*a2 a2 ...

and in a big endian CPU as:

a1 r1*a1 b1*a1 g1*a1 a2 r2*a2 g2*a2 b2*a2 ...

Here is a version that works correctly on a little endian machine without the NumPy dependency:

def pil2cairo(im):
    """Transform a PIL Image into a Cairo ImageSurface."""

    assert sys.byteorder == 'little', 'We don\'t support big endian'
    if im.mode != 'RGBA':
        im = im.convert('RGBA')

    s = im.tostring('raw', 'BGRA')
    a = array.array('B', s)
    dest = cairo.ImageSurface(cairo.FORMAT_ARGB32, im.size[0], im.size[1])
    ctx = cairo.Context(dest)
    non_premult_src_wo_alpha = cairo.ImageSurface.create_for_data(
        a, cairo.FORMAT_RGB24, im.size[0], im.size[1])
    non_premult_src_alpha = cairo.ImageSurface.create_for_data(
        a, cairo.FORMAT_ARGB32, im.size[0], im.size[1])
    ctx.set_source_surface(non_premult_src_wo_alpha)
    ctx.mask_surface(non_premult_src_alpha)
    return dest

Here I do the premultiplication with cairo. I also tried doing the premultiplication with NumPy but the result was slower. This function takes, in my computer (Mac OS X, 2.13GHz Intel Core 2 Duo) ~1s to convert an image of 6000x6000 pixels, and 5ms to convert an image of 500x500 pixels.

Hereunder answered 12/9, 2012 at 8:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.