Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow image_frombuffer to understand cairos premultiplied ARGB32 #2972

Closed
stuaxo opened this issue Jan 5, 2022 · 45 comments
Closed

Allow image_frombuffer to understand cairos premultiplied ARGB32 #2972

stuaxo opened this issue Jan 5, 2022 · 45 comments
Labels

Comments

@stuaxo
Copy link
Contributor

stuaxo commented Jan 5, 2022

It would be good if image_frombuffer could understand pycairos premultiplied ARGB32 format, currently it seems impossible to use, as pygame is doing some byteswapping and it's not obvious how to enable alpha premultiplication.

In detail:

@hanysz found an interesting issue using pycairo to create surfaces for pygame pygobject/pycairo#247

Cairos ARGB32 format stores ARGB starting in the high byte, with alpha premultiplied.

The docs make it look like image_frombuffer should be able to handle this format, but unfortunately there is some
undesired byteswapping happening.

The implementation calls SDL_CreateRGBSurfaceFrom, which looks like it could be given the right parameters to understand the format.

@hanysz test prog creates an image in cairo, rendering text in red, green and blue and renders in pygame - but only the "red" text is output, in blue:

image

The feature request:

Possibly this needs a new format: native endian RGBA32 with alpha premultiplication, or maybe it's a matter of providing a method of disabling the byteswapping.

@Starbuck5
Copy link
Contributor

Starbuck5 commented Jan 12, 2022

Hello, I've started to do some research on this issue.

I found a couple people with various integration solutions: (They all seem complicated 😅)
https://stackoverflow.com/questions/11640161/byte-order-when-using-cairo-in-pygame-sdl
https://www.pygame.org/wiki/CairoPygame

I also found a Cairo page on the matter:
https://www.cairographics.org/SDL/
They point to using another C library to handle the interop, which was last updated in 2009. I find it kind of funny that we are both here as Python libraries trying to do better integration than our respective C libraries.

In terms of byte swapping, I devised a test program and it seems like Cairo is doing the swapping.
Test program:

import pygame
import cairo

surf2 = cairo.ImageSurface(cairo.FORMAT_ARGB32, 2, 1)
ctx = cairo.Context(surf2)
ctx.rectangle(0, 0, 2, 1)
ctx.set_source_rgb(0, 0.5, 0.5)
ctx.fill_preserve()
print(bytes(surf2.get_data()))
print([int(i) for i in bytes(surf2.get_data())])

null = bytes(8)
surf3 = pygame.image.frombuffer(null, (2, 1), "ARGB")
surf3.fill((0, 128, 128))
print(null)
print([int(i) for i in null])

Output (Windows 10, Pygame latest, PyCairo latest):

b'\x80\x80\x00\xff\x80\x80\x00\xff'
[128, 128, 0, 255, 128, 128, 0, 255]
b'\xff\x00\x80\x80\xff\x00\x80\x80'
[255, 0, 128, 128, 255, 0, 128, 128]

In terms of alpha premultiplication, I found that SDL does not have support for it, as Cairo's SDL integration page stated. However, pygame has a special blend flag for Surface.blit called BLEND_PREMULTIPLIED. That doesn't apply to the simple example above, since at full alpha the premultiplication is not any different from non multiplied. I'm having weird test results with colors across the transition, but if we figure that out, and BLEND_PREMULTIPLIED works, that would be a great addition to the pygame integration example in the Cairo repository. Another enhancement for the example could be a more conventional pygame event loop, so that the window closing will be handled properly, etc.

@Starbuck5 Starbuck5 added the image pygame.image label Jan 12, 2022
@illume
Copy link
Member

illume commented Jan 12, 2022

pygame would need a "XRGB" here that would give an RGB surface?

Because reasons:

  • the alpha part can be ignored
  • RGB data is already modified based on the alpha.

aside: With SDL 1, messing around with the masks could get you there. I wonder if from* should support mask twiddling.

@stuaxo
Copy link
Contributor Author

stuaxo commented Jan 12, 2022

These both seem like slightly annoying but solvable, let's look at byte order.

Is the order of the bytes from Cairo just a result of x86 being little-endian ? (I always found this a bit confusing !)

If I was setup to build pygame I'd have a poke at this, and try flipping the logic at #if SDL_BYTEORDER == SDL_LIL_ENDIAN and see what the output looks like (not a correct solution, but could point at something).

@stuaxo
Copy link
Contributor Author

stuaxo commented Jan 12, 2022

A bit more info here on cairo Image Surfaces in ARGB32 format: https://cairographics.org/manual/cairo-Image-Surfaces.html

CAIRO_FORMAT_ARGB32 each pixel is a 32-bit quantity, with alpha in the upper 8 bits, then red, then green, then blue. The 32-bit quantities are stored native-endian. Pre-multiplied alpha is used. (That is, 50% transparent red is 0x80800000, not 0x80ff0000.) (Since 1.0)

@Starbuck5
Copy link
Contributor

I was/am having trouble wrapping my head around it, but I found a very helpful wiki page for this: https://handwiki.org/wiki/RGBA_color_space

In computer graphics, pixels encoding the RGBA color space information must be stored in computer memory (or in files on disk), in well defined formats. There are several ways to encode RGBA colors, which can lead to confusion when image data is exchanged. These encodings are often denoted by the four letters in some order (e.g. RGBA, ARGB, etc.). Unfortunately, the interpretation of these 4-letter mnemonics is not well established, leading to further confusion. There are two typical ways to understand a mnemonic such as "RGBA":

In the byte-order scheme, "RGBA" is understood to mean a byte R, followed by a byte G, followed by a byte B, and followed by a byte A. This scheme is commonly used for describing file formats or network protocols, which are both byte-oriented.
In the word-order scheme, "RGBA" is understood to represent a complete 32-bit word, where R is more significant than G, which is more significant than B, which is more significant than A. This scheme can be used to describe the memory layout on a particular system. Its meaning varies depending on the endianness of the system.

There's also some tables and diagrams on the wiki page beyond these quotes, so check those out.

So I think pygame is using the byte order scheme, and Cairo is using the word order scheme.

@Starbuck5
Copy link
Contributor

Starbuck5 commented Jan 13, 2022

So maybe SDL uses the word order scheme, and that's why pygame's functions can reverse the order on different endianness?

We could close this issue by adding new format arguments specifically for the word order scheme. Like "ARGB_w"?

Have any other libraries faced this yet and implemented something? What's PIL doing?

Edit, this is what PIL is doing: https://pillow.readthedocs.io/en/stable/handbook/concepts.html#concept-modes

Maybe what Cairo wants from ARGB is PIL's mode I.

@stuaxo
Copy link
Contributor Author

stuaxo commented Jan 13, 2022

It looks like PIL has lowercase i for premultiplied alpha.

Cairo also has just RGB which is 3 bytes and 1 for padding, this might be less confusing to test with initially.

I should probably write some tests to ensure the examples on the Cairo integration page are correct.

There's PIL to Cairo there (but not the other way round), in the example it's called BGRa, but as mentioned I should test if it's correct.

@illume
Copy link
Member

illume commented Jan 16, 2022

In SDL 2 setting the masks is not allowed on Surfaces. Previously this issue could have been handled with a Surface.set_mask call. In pygame 2 set_mask doesn't work. We may be able to get it to work by creating a new underlying SDL Surface on the python object. pygame 2 broke various adapters including a opencv one because set_masks stopped working.

As well, we may want to consider adding a mask argument to frombuffer/tobuffer.

@MyreMylar
Copy link
Contributor

I wonder if we could make sort of quick mapping from a python function on surface called something like 'shuffle_channels(from, to)' then pass it over to:

_mm256_shuffle_epi8()

(An AVX2 intrinsic) and pretty efficiently rejig the channels for any surface. Essentially at the important part you have:

//RGBA32 -> ARGB32
_mm256_set_epi8( 28, 20, 29, 31,
                 24, 26, 25, 27,
                 20, 22, 21, 23,
                 16, 18, 17, 19,
                 12, 14, 13, 15,
                 8, 10, 9, 11, 
                 4, 6, 5, 7, 
                 0, 2, 1, 3);

Would do 8 pixels per loop of the surface, and you'd need a one pixel version to finish off any rows. We could likely quickly make a bunch of these as the rest of the code around it would be a fairly boilerplate operation. Then you'd need the slow non-SIMD version for the non-SIMD platforms.

An idea anyway.

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 21, 2022

Cairo's ARGB32 format is BGRA in pygame format.
But pygame doesn't offer BGRA :)

Using BGR as the example,
https://github.com/pygame/pygame/blob/main/src_c/image.c#L1141

BGRA would look something like,

    else if (!strcmp(format, "BGRA")) {
        if (len != (Py_ssize_t)w * h * 4)
            return RAISE(
                PyExc_ValueError,
                "Buffer length does not equal format and resolution size");
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
        surf = SDL_CreateRGBSurfaceFrom(data, w, h, 32, w * 4,
                                        0xFF << 16,
                                        0xFF << 8,
                                        0xFF,
                                        0xFF << 24);

#else
        surf = SDL_CreateRGBSurfaceFrom(data, w, h, 32, w * 4,
                                        0xFF << 8,
                                        0xFF << 16,
                                        0xFF << 24,
                                        0xFF );
#endif
    }

ref.
SDL_CreateRGBSurfaceFrom

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 22, 2022

I compiled pygame with the changes to image.c, as I outlined above.
Success!

However, not fully tested!!

pygame/pycairo code to use will be,

# Create Cairo surface
cairo_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
...
...
# Create PyGame surface
 pygame_surface = pygame.image.frombuffer(cairo_surface.get_data(), (width, height), 'BGRA')
bandicam.2022-07-23.02-54-07-117.mp4

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 22, 2022

It should be noted that the video is showing some compression artifacts.
But indeed it is all nicely antialised,
2022-07-23 03_03_52-'BGRA' format added to image c - 2022-07-23

@stuaxo
Copy link
Contributor Author

stuaxo commented Jul 22, 2022

Fantastic :)

I wonder it's possible to make BGRA_PREMULT, by starting from one of the existing premultiplied examples in image.c, like ARGB_PREMULT ?

That would make alpha compatible.

Cairo also has non alpha version, in effect - BGR, which I believe is basically the same 32 bit format, but the alpha channel is not populated.

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 22, 2022

Using the example for RGBA_PREMULT,
image.c#L756

 else if (!strcmp(format, "RGBA_PREMULT")) {
...
}

and switching R & B values.

Seems straightforward.

But that is code for,
pygame.image.tostring()

To do this within,
pygame.image.frombuffer()

is not as simple.

The existing code isn't manipulating the data in any way.
You are simply feeding the C function,
SDL_CreateRGBSurfaceFrom()
masks to indicate the position of the R, G, B & A values.

Therefore a premult, would require adding code to multiply each R, G, & B value by the A value prior to feeding it to SDL_CreateRGBSurfaceFrom().

It is costly,
e.g.
image.c#L771

that is looping over the entire data and performing the multiplication.

Alternatively, but same cost, have standalone generic converter() function that you call in pygame before calling pygame.image.frombuffer()

This is along the lines of what was proposed by MyreMylar

e.g.
converter(data, source_RGBA_format, destination_RGBA_format, premult=False).

The nice thing about that is it would be one function for all formats.

@Starbuck5
Copy link
Contributor

Looks like some great work @rlatowicz!

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 23, 2022

Looking at this,
pil-prevent-premultiplied-alpha-channel
and
OpenImageIO - premult, unpremult, ...

It might be useful to some to offer an unpremult option,

converter(data, source_RGBA_format, destination_RGBA_format, premult_type = no_premult)
premult_type is either no_premult, premult or un_premult

@MyreMylar
Copy link
Contributor

MyreMylar commented Jul 23, 2022

You mathematically can't unpremultiply reliably with only 8 bit colour channel data. See:

Red channel = 255
Green channel = 200
Blue channel = 150
Alpha channel = 1

pre alpha premul:
(255, 200, 150, 1)

post alpha premul (these four 8 bit values are all you have per pixel):
(1, 1, 1, 1)

best reversal attempt:
(255, 255, 255, 1)

You either keep a second surface worth of data with the original channel data (wasteful of memory, so not going to happen) or you would have to store channel data at a much higher resolution - and even then it wouldn't be perfectly reversible.

Also - alpha premultiplied is a perfectly usable (in fact superior) format in pygame anyway so there is no real need to attempt a reversal. Just use the BLEND_PREMULTIPLIED flag when blitting.

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 23, 2022

@MyreMylar

I agree.

In regards to the converter, it should probably offer both an inplace conversion (faster) as well as the option to return a new buffer.

Or separate functions - e.g. converter() and converter.frombuffer() ?

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 23, 2022

Using @Starbuck5 's example above, this is a minimal workbench for testing a premult,

import pygame
import cairo

def premult(din):
    dout = bytearray(len(din))
    pixel_count = len(din) >> 2
    for i in range(pixel_count):
        i4 = i << 2
        alpha = din[i4 + 3]
        dout[i4] = din[i4] * alpha >> 8
        dout[i4 + 1] = din[i4 + 1] * alpha >> 8
        dout[i4 + 2] = din[i4 + 2] * alpha >> 8
        dout[i4 + 3] = alpha
    return dout

surf2 = cairo.ImageSurface(cairo.FORMAT_ARGB32, 2, 1)
ctx = cairo.Context(surf2)
ctx.rectangle(0, 0, 2, 1)
ctx.set_source_rgba(0.2, 0.4, 0.6, 0.5)
ctx.fill_preserve()

print(bytes(surf2.get_data()))
print([int(i) for i in bytes(surf2.get_data())])

data2 = premult(surf2.get_data())

surf3 = pygame.image.frombuffer(data2, (2, 1), "BGRA")
print()
print(bytes(surf3.get_buffer()))
print([int(i) for i in bytes(surf3.get_buffer())])


output is,

b'L3\x19\x80L3\x19\x80'
[76, 51, 25, 128, 76, 51, 25, 128]

b'&\x19\x0c\x80&\x19\x0c\x80'
[38, 25, 12, 128, 38, 25, 12, 128]

current premult() function is just a placeholder for a C/assembler version

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 23, 2022

The inplace version,

import pygame
import cairo

def premult_inplace(din):
    pixel_count = len(din) >> 2
    for i in range(pixel_count):
        i4 = i << 2
        alpha = din[i4 + 3]
        din[i4] = din[i4] * alpha >> 8
        din[i4 + 1] = din[i4 + 1] * alpha >> 8
        din[i4 + 2] = din[i4 + 2] * alpha >> 8
    
surf2 = cairo.ImageSurface(cairo.FORMAT_ARGB32, 2, 1)
ctx = cairo.Context(surf2)
ctx.rectangle(0, 0, 2, 1)
ctx.set_source_rgba(0.2, 0.4, 0.6, 0.5)
ctx.fill_preserve()

print(bytes(surf2.get_data()))
print([int(i) for i in bytes(surf2.get_data())])

premult_inplace(surf2.get_data())

surf3 = pygame.image.frombuffer(surf2.get_data(), (2, 1), "BGRA")
print()
print(bytes(surf3.get_buffer()))
print([int(i) for i in bytes(surf3.get_buffer())])

output again,

b'L3\x19\x80L3\x19\x80'
[76, 51, 25, 128, 76, 51, 25, 128]

b'&\x19\x0c\x80&\x19\x0c\x80'
[38, 25, 12, 128, 38, 25, 12, 128]

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 23, 2022

This was added to SDL in Nov 2021,
SDL_PremultiplyAlpha

and

sdl-added-sdl-premultiplyalpha-to-premultiply-alpha-on-a-block-of-sdl-pixelformat-argb8888-pixels

Note that ARGB32 is an alias for ARGB8888,
SDL_PixelFormatEnum

But it hasn't been wrapped by pygame code.

Code is here,
SDL_PremultiplyAlpha()

@rlatowicz
Copy link
Contributor

I got this working,

2022-07-24 12_45_32-D__Coding_Work_cairo_graphics_pygame_pycairo_looking_at bytes_004 py - SciTE

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 24, 2022

It's hackish but it was the path of least resistance.

I added a BGRA_PREMULT to image.frombuffer(). This was simply a copy and paste of what I indicated above for BGRA.

I added to image.c the existing code for SDL_PremultiplyAlpha() from SDL_surface.c

I added to image.c a couple defines from SDL_blit.h that are used in SDL_PremultiplyAlpha(),

#define ARGB8888_FROM_RGBA(Pixel, r, g, b, a)                           \
{                                                                       \
    Pixel = (a<<24)|(r<<16)|(g<<8)|b;                                   \
}
#define RGBA_FROM_ARGB8888(Pixel, r, g, b, a)                           \
{                                                                       \
    r = ((Pixel>>16)&0xFF);                                             \
    g = ((Pixel>>8)&0xFF);                                              \
    b = (Pixel&0xFF);                                                   \
    a = (Pixel>>24);                                                    \
}

Then added the following code to the BGRA_PREMULT section just before the call to SDL_CreateRGBSurfaceFrom(),

int pma_reult = SDL_PremultiplyAlpha(w, h,
                         SDL_PIXELFORMAT_BGRA32, data, w*4,
                         SDL_PIXELFORMAT_BGRA32, data, w*4);

Apologies for lack of comprehensive error checking. This was a quick and dirty test.
Compiled and then ran the minimal workbench code modified to use the BGRA_PREMULT option,

import pygame
import cairo

surf2 = cairo.ImageSurface(cairo.FORMAT_ARGB32, 2, 1)
ctx = cairo.Context(surf2)
ctx.rectangle(0, 0, 2, 1)
ctx.set_source_rgba(0.2, 0.4, 0.6, 0.5)
ctx.fill_preserve()
print()
print(bytes(surf2.get_data()))
print([int(i) for i in bytes(surf2.get_data())])

surf3 = pygame.image.frombuffer(surf2.get_data(), (2, 1), "BGRA_PREMULT")
print()
print(bytes(surf3.get_buffer()))
print([int(i) for i in bytes(surf3.get_buffer())])

Output,

b'L3\x19\x80L3\x19\x80'
[76, 51, 25, 128, 76, 51, 25, 128]

b'&\x19\x0c\x80&\x19\x0c\x80'
[38, 25, 12, 128, 38, 25, 12, 128]

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 24, 2022

Here is the full image.c

Like I said, not a finished product and this is only the inplace premult.
The non mutating version is straightforward at this point.

SDL_PremultiplyAlpha() isn't optimized.
optimization

@MyreMylar
Copy link
Contributor

There is a PR open already to add an optimised premul_alpha() method to pygame Surfaces, if that is what you are trying to do:

#3276

Uses SSE2 at the minute but I will likely also add an AVX2 version before the PR gets merged.

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 24, 2022

I'm not sure what I am trying to do :)

I started simply wanting to have have pygame receive a pycairo image without having to do an intermediary step.
I was using PIL in the intermediary step.

Adding the 'BGRA' code solved that.

I thought the 'BGRA_PREMULT' was wanted by others in this thread but it makes no sense to me :)
The cairo data is already pre-multed.

Why on earth would you want to premult again?

I guess I want clarification on the use case.
Was there an intention to receive unadulterated data and premult it on the pygame side?

@MyreMylar
Copy link
Contributor

I'm not sure :) but you seemed to be trying to do that.

I'm sure we'd be happy to accept a PR for image.frombuffer() adding BGRA to the list of formats it accepts if you want to submit it, or we can try and find somebody else to mine this discussion thread.

@rlatowicz
Copy link
Contributor

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 25, 2022

I have tentatively added fromstring and tostring versions of BGRA.
They are working in superficial (visual) testing.
The official tests haven't been fully written yet.
They are more involved than the what I had to do for frombuffer()

But a simply addition to image_test.py of the following which is testing the symmetry of tostring & fromstring for a given format,

       for fmt in ("ARGB", "RGBA", "BGRA"):
            fmt_buf = pygame.image.tostring(test_surface, fmt)
            test_to_from_fmt_string = pygame.image.fromstring(
                fmt_buf, test_surface.get_size(), fmt
            )

            self._assertSurfaceEqual(
                test_surface,
                test_to_from_fmt_string,
                "tostring/fromstring functions are not "
                'symmetric with "{}" format'.format(fmt),
            )

passes!

pygame 2.1.3.dev4 (SDL 2.0.20, Python 3.9.13)
Hello from the pygame community. https://www.pygame.org/contribute.html
....................................
----------------------------------------------------------------------
Ran 36 tests in 0.486s

OK

However, in case 4: for tostring in image.c, the function tostring_surf_32bpp() is used in both RGBA and ARGB.
It can use SSE4.2 instructions, if available
It's last two parameters are color_offset and alpha_offset.
In RGBA's case would be 0 & 3.

The way that function is written, color_offset is pointing to the start of an RGB triplet.
This works for RGBA & ARGB but not for BGRA.

I'm doing it the slower way for now,

            case 4:
                for (h = 0; h < surf->h; ++h) {
                    Uint32 *ptr = (Uint32 *)DATAROW(surf->pixels, h,
                    surf->pitch, surf->h, flipped);
                    for (w = 0; w < surf->w; ++w) {
                        color = *ptr++;
                        data[2] = (char)(((color & Rmask) >> Rshift) << Rloss);
                        data[1] = (char)(((color & Gmask) >> Gshift) << Gloss);
                        data[0] = (char)(((color & Bmask) >> Bshift) << Bloss);
                        data[3] = (char)(Amask ? (((color & Amask) >> Ashift)
                                                  << Aloss)
                                               : 255);
                        data += 4;
                    }                }
                break;

as opposed to something like,

            case 4:
                tostring_surf_32bpp(surf, flipped, hascolorkey, colorkey, data,
                                    0, 3);
                break;
        }

Having looked at it a bit closer, I see how to adapt tostring_surf_32bpp() or add a new one that will work with BGRA.

For another time.

@rlatowicz
Copy link
Contributor

rlatowicz commented Jul 25, 2022

@stuaxo
Copy link
Contributor Author

stuaxo commented Jul 25, 2022

I thought the 'BGRA_PREMULT' was wanted by others in this thread but it makes no sense to me :)
The cairo data is already pre-multed.

Why on earth would you want to premult again?

This might be my fault for coming back and not getting myself back up to speed again quickly enough, apologies !

Amazing work on all this, I always liked the idea these libraries could interoperate.

@radarhere
Copy link

The PR has been merged. Does that mean this issue is resolved?

@rlatowicz
Copy link
Contributor

Yes.

BGRA format added to frombuffer, tostring (tobytes) & fromstring (frombytes).

A usage e.g.,

# Create Cairo surface
cairo_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
...
...
# Create PyGame surface
 pygame_surface = pygame.image.frombuffer(cairo_surface.get_data(), (width, height), 'BGRA')

@Starbuck5
Copy link
Contributor

One thing I could see potentially remaining in this issue is documenting this somewhere. Not sure if there would be a good to place it.

Maybe a small pygame - cairo interoperability demo in examples/, or a note in the image docs that the BGRA mode is handy for cairo?

@rlatowicz
Copy link
Contributor

rlatowicz commented Aug 2, 2022

Is this too elaborate/busy?

pygame_pycairo_demo.py

# 2022_08  - pygame/pycairo direct interoperability via pygame's "BGRA" format

from math import pi, sin, cos
import pygame
import cairo
import time

width, height = 500, 500
running=True
fps=80
angle=0
speed=2

# Draw to Pycairo context
def draw(ctx, x, y, angle):
    
    ctx.scale(1, 1)
    ctx.set_source_rgb(1, 1, 1)
    ctx.paint()
 
    ctx.set_source_rgba(1,0,0,0.6)
    ctx.set_line_width(20.0+10*cos(angle))
    ctx.arc(x, y, 100, angle-pi/2, angle)
    ctx.stroke()

    ctx.set_source_rgba(0,1,1,.8)
    ctx.set_line_width(30.0+10*sin(angle))
    ctx.arc(x,y, 140, -(angle+pi), -angle)
    ctx.stroke()

    ctx.set_source_rgba(0, cos(angle), sin(angle), 0.5)
    ctx.set_line_width(20.0)
    ctx.move_to(x, y)
    ctx.line_to(x+150*cos(2*angle), y+150*sin(angle))
    ctx.stroke()

# Init PyGame
pygame.display.init()
screen = pygame.display.set_mode((width, height), 0, 32)
pygame.display.set_caption("pygame/pycairo interoperability")

# Pycairo surface
cairo_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)

# Pycairo context
ctx = cairo.Context(cairo_surface)

clock = pygame.time.Clock()

t1=time.perf_counter()
while running:
    clock.tick(fps)
    for event in pygame.event.get():        
        if event.type == pygame.QUIT:
            running = False      
    
    t2 = time.perf_counter()
    
    angle += speed*(t2-t1)
    angle = angle % (2*pi)
    
    x, y = pygame.mouse.get_pos()
    
    # Draw with Cairo on the surface
    draw(ctx, x, y, angle)
            
    # Create PyGame surface from the cairo surface buffer
    pygame_surface = pygame.image.frombuffer(cairo_surface.get_data(), (width, height), 'BGRA')

    # Alternatively using fromstring() or its synonym, frombytes()
    # pygame_surface = pygame.image.frombytes(cairo_surface.get_data().tobytes(), (width, height), 'BGRA')
    
    # Show PyGame surface
    screen.blit(pygame_surface, (0,0)) 
    
    pygame.display.flip()
    t1 = time.perf_counter()

also shows that pygame events work as normal and can be used in interacting with the cairo drawing.

I just added a comment showing the alternate usage with frombytes() or fromstring()

@stuaxo
Copy link
Contributor Author

stuaxo commented Aug 2, 2022

^ Just a little nit - did you mean to have .paint() and .fill() ?

@rlatowicz
Copy link
Contributor

After a bunch of edits that happens :)
Fixed.

Thanks.

@rlatowicz
Copy link
Contributor

rlatowicz commented Aug 2, 2022

What about this,
2022-08-02 23_33_30-pygame_pycairo interoperability

# 2022_08  - pygame/pycairo direct interoperability via pygame's "BGRA" format
from math import pi, sin, cos
import pygame
import cairo
import time

width, height = 500, 500
running=True
fps=80
angle=0
speed=2

# Draw to Pycairo context
def draw(ctx, x, y, angle):
    ctx.scale(1, 1)
    ctx.set_source_rgb(1, 1, 1)
    ctx.paint()
    
    ctx.set_source_rgba(1, 0, 0)
    ctx.set_line_width(20.0+10*cos(angle))
    ctx.arc(x, y, 60, angle-pi/2, angle)
    ctx.stroke()

    ctx.set_source_rgba(0, 1, 1)
    ctx.set_line_width(30.0+10*sin(angle))
    ctx.arc(x, y, 100, -(angle+pi), -angle)
    ctx.stroke()
    
    ctx.set_source_rgba(0, 0, 0, 0.5)
    ctx.move_to(x, y-20)
    ctx.text_path("pygame/pycairo")
    ctx.fill()

# Init PyGame
pygame.display.init()
screen = pygame.display.set_mode((width, height), 0, 32)
pygame.display.set_caption("pygame/pycairo interoperability")

# Pycairo surface
cairo_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)

# Pycairo context
ctx = cairo.Context(cairo_surface)
ctx.set_font_size(42)

clock = pygame.time.Clock()

t1=time.perf_counter()
while running:
    clock.tick(fps)
    for event in pygame.event.get():        
        if event.type == pygame.QUIT:
            running = False      
    
    t2 = time.perf_counter()
    
    angle += speed*(t2-t1)
    angle = angle % (2*pi)
    
    x, y = pygame.mouse.get_pos()
    
    # Draw with Cairo on the surface
    draw(ctx, x, y, angle)
            
    # Create PyGame surface from the cairo surface buffer
    pygame_surface = pygame.image.frombuffer(cairo_surface.get_data(), (width, height), 'BGRA')
    
    # Alternatively
    # pygame_surface = pygame.image.frombytes(cairo_surface.get_data().tobytes(), (width, height), 'BGRA')
    
    # Show PyGame surface
    screen.blit(pygame_surface, (0,0)) 
    
    pygame.display.flip()
    t1 = time.perf_counter()

@stuaxo
Copy link
Contributor Author

stuaxo commented Aug 2, 2022

Nice :)

There are a lot of possibilities for things like interesting HUDs using cairo, or games using curves - there are the blend modes for filling and painting and stuff like gradient fills - I think this opens up a new world of interesting things.

@MyreMylar
Copy link
Contributor

# Show PyGame surface
    screen.blit(pygame_surface, (0,0)) 

shouldn't this line use the BLEND_PREMULTIPLIED special_flag?

@rlatowicz
Copy link
Contributor

rlatowicz commented Aug 3, 2022

@MyreMylar

screen.blit(pygame_surface, (0,0))  # Default blend
vs
screen.blit(pygame_surface, (0,0), special_flags=pygame.BLEND_PREMULTIPLIED)
vs
screen.blit(pygame_surface, (0,0), special_flags=pygame.BLEND_ALPHA_SDL2)

There won't be a difference using any of those 3, for the example given?
However, the other 2 might be faster.

Why would you use it here?
I can see an argument for it, to indicate that the pycairo data is premultiplied.

@MyreMylar
Copy link
Contributor

I can see an argument for it, to indicate that the pycairo data is premultiplied.

This was my thought, even if the surface has no actual alpha it is an indication of the format of the data - though perhaps a comment would also be useful?

@stuaxo
Copy link
Contributor Author

stuaxo commented Aug 3, 2022

I wonder if it's worth having some images with alpha using layering as a demo ?

This would let people see that the premultiplied alpha worked.

@illume
Copy link
Member

illume commented Aug 20, 2022

Can this issue be closed now?

@stuaxo
Copy link
Contributor Author

stuaxo commented Sep 4, 2022

I think so, though I guess an example "would be nice", that might be worth adding as another ticket.

I finally tried the example and it's fun :)

I'll shut this now (I'd forgotten that I opened it by now!) - it looks like there are some other tickets that have split off this, but they can probably have their own life now.

@stuaxo stuaxo closed this as completed Sep 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants