/*
 * The Python Imaging Library.
 *
 * display support (and other windows-related stuff)
 *
 * History:
 * 1996-05-13 fl  Windows DIB support
 * 1996-05-21 fl  Added palette stuff
 * 1996-05-28 fl  Added display_mode stuff
 * 1997-09-21 fl  Added draw primitive
 * 2001-09-17 fl  Added ImagingGrabScreen (from _grabscreen.c)
 * 2002-05-12 fl  Added ImagingListWindows
 * 2002-11-19 fl  Added clipboard support
 * 2002-11-25 fl  Added GetDC/ReleaseDC helpers
 * 2003-05-21 fl  Added create window support (including window callback)
 * 2003-09-05 fl  Added fromstring/tostring methods
 * 2009-03-14 fl  Added WMF support (from pilwmf)
 *
 * Copyright (c) 1997-2003 by Secret Labs AB.
 * Copyright (c) 1996-1997 by Fredrik Lundh.
 *
 * See the README file for information on usage and redistribution.
 */

#define PY_SSIZE_T_CLEAN
#include "Python.h"

#include "libImaging/Imaging.h"

/* -------------------------------------------------------------------- */
/* Windows DIB support */

#ifdef _WIN32

#include "libImaging/ImDib.h"

#if SIZEOF_VOID_P == 8
#define F_HANDLE "K"
#else
#define F_HANDLE "k"
#endif

typedef struct {
    PyObject_HEAD ImagingDIB dib;
} ImagingDisplayObject;

static PyTypeObject ImagingDisplayType;

static ImagingDisplayObject *
_new(const ModeID mode, int xsize, int ysize) {
    ImagingDisplayObject *display;

    if (PyType_Ready(&ImagingDisplayType) < 0) {
        return NULL;
    }

    display = PyObject_New(ImagingDisplayObject, &ImagingDisplayType);
    if (display == NULL) {
        return NULL;
    }

    display->dib = ImagingNewDIB(mode, xsize, ysize);
    if (!display->dib) {
        Py_DECREF(display);
        return NULL;
    }

    return display;
}

static void
_delete(ImagingDisplayObject *display) {
    if (display->dib) {
        ImagingDeleteDIB(display->dib);
    }
    PyObject_Del(display);
}

static PyObject *
_expose(ImagingDisplayObject *display, PyObject *args) {
    HDC hdc;
    if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) {
        return NULL;
    }

    ImagingExposeDIB(display->dib, hdc);

    Py_RETURN_NONE;
}

static PyObject *
_draw(ImagingDisplayObject *display, PyObject *args) {
    HDC hdc;
    int dst[4];
    int src[4];
    if (!PyArg_ParseTuple(
            args,
            F_HANDLE "(iiii)(iiii)",
            &hdc,
            dst + 0,
            dst + 1,
            dst + 2,
            dst + 3,
            src + 0,
            src + 1,
            src + 2,
            src + 3
        )) {
        return NULL;
    }

    ImagingDrawDIB(display->dib, hdc, dst, src);

    Py_RETURN_NONE;
}

extern Imaging
PyImaging_AsImaging(PyObject *op);

static PyObject *
_paste(ImagingDisplayObject *display, PyObject *args) {
    Imaging im;

    PyObject *op;
    int xy[4];
    xy[0] = xy[1] = xy[2] = xy[3] = 0;
    if (!PyArg_ParseTuple(args, "O|(iiii)", &op, xy + 0, xy + 1, xy + 2, xy + 3)) {
        return NULL;
    }
    im = PyImaging_AsImaging(op);
    if (!im) {
        return NULL;
    }

    if (xy[2] <= xy[0]) {
        xy[2] = xy[0] + im->xsize;
    } else if (xy[2] - xy[0] != im->xsize) {
        return ImagingError_Mismatch();
    }
    if (xy[3] <= xy[1]) {
        xy[3] = xy[1] + im->ysize;
    } else if (xy[3] - xy[1] != im->ysize) {
        return ImagingError_Mismatch();
    }

    ImagingPasteDIB(display->dib, im, xy);

    Py_RETURN_NONE;
}

static PyObject *
_query_palette(ImagingDisplayObject *display, PyObject *args) {
    HDC hdc;
    int status;

    if (!PyArg_ParseTuple(args, F_HANDLE, &hdc)) {
        return NULL;
    }

    status = ImagingQueryPaletteDIB(display->dib, hdc);

    return Py_BuildValue("i", status);
}

static PyObject *
_getdc(ImagingDisplayObject *display, PyObject *args) {
    HWND window;
    HDC dc;

    if (!PyArg_ParseTuple(args, F_HANDLE, &window)) {
        return NULL;
    }

    dc = GetDC(window);
    if (!dc) {
        PyErr_SetString(PyExc_OSError, "cannot create dc");
        return NULL;
    }

    return Py_BuildValue(F_HANDLE, dc);
}

static PyObject *
_releasedc(ImagingDisplayObject *display, PyObject *args) {
    HWND window;
    HDC dc;

    if (!PyArg_ParseTuple(args, F_HANDLE F_HANDLE, &window, &dc)) {
        return NULL;
    }

    ReleaseDC(window, dc);

    Py_RETURN_NONE;
}

static PyObject *
_frombytes(ImagingDisplayObject *display, PyObject *args) {
    Py_buffer buffer;

    if (!PyArg_ParseTuple(args, "y*:frombytes", &buffer)) {
        return NULL;
    }

    if (display->dib->ysize * display->dib->linesize != buffer.len) {
        PyBuffer_Release(&buffer);
        PyErr_SetString(PyExc_ValueError, "wrong size");
        return NULL;
    }

    memcpy(display->dib->bits, buffer.buf, buffer.len);

    PyBuffer_Release(&buffer);
    Py_RETURN_NONE;
}

static PyObject *
_tobytes(ImagingDisplayObject *display, PyObject *args) {
    if (!PyArg_ParseTuple(args, ":tobytes")) {
        return NULL;
    }

    return PyBytes_FromStringAndSize(
        display->dib->bits, display->dib->ysize * display->dib->linesize
    );
}

static struct PyMethodDef methods[] = {
    {"draw", (PyCFunction)_draw, METH_VARARGS},
    {"expose", (PyCFunction)_expose, METH_VARARGS},
    {"paste", (PyCFunction)_paste, METH_VARARGS},
    {"query_palette", (PyCFunction)_query_palette, METH_VARARGS},
    {"getdc", (PyCFunction)_getdc, METH_VARARGS},
    {"releasedc", (PyCFunction)_releasedc, METH_VARARGS},
    {"frombytes", (PyCFunction)_frombytes, METH_VARARGS},
    {"tobytes", (PyCFunction)_tobytes, METH_VARARGS},
    {NULL, NULL} /* sentinel */
};

static PyObject *
_getattr_mode(ImagingDisplayObject *self, void *closure) {
    return Py_BuildValue("s", getModeData(self->dib->mode)->name);
}

static PyObject *
_getattr_size(ImagingDisplayObject *self, void *closure) {
    return Py_BuildValue("ii", self->dib->xsize, self->dib->ysize);
}

static struct PyGetSetDef getsetters[] = {
    {"mode", (getter)_getattr_mode}, {"size", (getter)_getattr_size}, {NULL}
};

static PyTypeObject ImagingDisplayType = {
    PyVarObject_HEAD_INIT(NULL, 0).tp_name = "ImagingDisplay",
    .tp_basicsize = sizeof(ImagingDisplayObject),
    .tp_dealloc = (destructor)_delete,
    .tp_methods = methods,
    .tp_getset = getsetters,
};

PyObject *
PyImaging_DisplayWin32(PyObject *self, PyObject *args) {
    ImagingDisplayObject *display;
    char *mode_name;
    int xsize, ysize;

    if (!PyArg_ParseTuple(args, "s(ii)", &mode_name, &xsize, &ysize)) {
        return NULL;
    }

    const ModeID mode = findModeID(mode_name);
    display = _new(mode, xsize, ysize);
    if (display == NULL) {
        return NULL;
    }

    return (PyObject *)display;
}

PyObject *
PyImaging_DisplayModeWin32(PyObject *self, PyObject *args) {
    int size[2];
    const ModeID mode = ImagingGetModeDIB(size);
    return Py_BuildValue("s(ii)", getModeData(mode)->name, size[0], size[1]);
}

/* -------------------------------------------------------------------- */
/* Windows screen grabber */

typedef HANDLE(__stdcall *Func_GetWindowDpiAwarenessContext)(HANDLE);
typedef HANDLE(__stdcall *Func_SetThreadDpiAwarenessContext)(HANDLE);

PyObject *
PyImaging_GrabScreenWin32(PyObject *self, PyObject *args) {
    int x = 0, y = 0, width = -1, height;
    int includeLayeredWindows = 0, screens = 0;
    HBITMAP bitmap;
    BITMAPCOREHEADER core;
    HDC screen, screen_copy;
    HWND wnd;
    DWORD rop;
    PyObject *buffer;
    HANDLE dpiAwareness = NULL;
    HMODULE user32;
    Func_GetWindowDpiAwarenessContext GetWindowDpiAwarenessContext_function;
    Func_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContext_function;

    if (!PyArg_ParseTuple(
            args, "|ii" F_HANDLE, &includeLayeredWindows, &screens, &wnd
        )) {
        return NULL;
    }

    /* step 1: create a memory DC large enough to hold the
       entire screen */

    if (screens == -1) {
        screen = GetDC(wnd);
        if (screen == NULL) {
            PyErr_SetString(PyExc_OSError, "unable to get device context for handle");
            return NULL;
        }
    } else {
        screen = CreateDC("DISPLAY", NULL, NULL, NULL);
    }
    screen_copy = CreateCompatibleDC(screen);

    // added in Windows 10 (1607)
    // loaded dynamically to avoid link errors
    user32 = LoadLibraryA("User32.dll");
    SetThreadDpiAwarenessContext_function = (Func_SetThreadDpiAwarenessContext)
        GetProcAddress(user32, "SetThreadDpiAwarenessContext");
    if (SetThreadDpiAwarenessContext_function != NULL) {
        GetWindowDpiAwarenessContext_function = (Func_GetWindowDpiAwarenessContext)
            GetProcAddress(user32, "GetWindowDpiAwarenessContext");
        if (screens == -1 && GetWindowDpiAwarenessContext_function != NULL) {
            dpiAwareness = GetWindowDpiAwarenessContext_function(wnd);
        }
        // DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ((DPI_CONTEXT_HANDLE)-3)
        dpiAwareness = SetThreadDpiAwarenessContext_function(
            dpiAwareness == NULL ? (HANDLE)-3 : dpiAwareness
        );
    }

    if (screens == 1) {
        x = GetSystemMetrics(SM_XVIRTUALSCREEN);
        y = GetSystemMetrics(SM_YVIRTUALSCREEN);
        width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
        height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
    } else if (screens == -1) {
        RECT rect;
        if (GetClientRect(wnd, &rect)) {
            width = rect.right;
            height = rect.bottom;
        }
    } else {
        width = GetDeviceCaps(screen, HORZRES);
        height = GetDeviceCaps(screen, VERTRES);
    }

    if (SetThreadDpiAwarenessContext_function != NULL) {
        SetThreadDpiAwarenessContext_function(dpiAwareness);
    }

    FreeLibrary(user32);

    if (width == -1) {
        goto error;
    }

    bitmap = CreateCompatibleBitmap(screen, width, height);
    if (!bitmap) {
        goto error;
    }

    if (!SelectObject(screen_copy, bitmap)) {
        goto error;
    }

    /* step 2: copy bits into memory DC bitmap */

    rop = SRCCOPY;
    if (includeLayeredWindows) {
        rop |= CAPTUREBLT;
    }
    if (!BitBlt(screen_copy, 0, 0, width, height, screen, x, y, rop)) {
        goto error;
    }

    /* step 3: extract bits from bitmap */

    buffer = PyBytes_FromStringAndSize(NULL, height * ((width * 3 + 3) & -4));
    if (!buffer) {
        return NULL;
    }

    core.bcSize = sizeof(core);
    core.bcWidth = width;
    core.bcHeight = height;
    core.bcPlanes = 1;
    core.bcBitCount = 24;
    if (!GetDIBits(
            screen_copy,
            bitmap,
            0,
            height,
            PyBytes_AS_STRING(buffer),
            (BITMAPINFO *)&core,
            DIB_RGB_COLORS
        )) {
        goto error;
    }

    DeleteObject(bitmap);
    DeleteDC(screen_copy);
    if (screens == -1) {
        ReleaseDC(wnd, screen);
    } else {
        DeleteDC(screen);
    }

    return Py_BuildValue("(ii)(ii)N", x, y, width, height, buffer);

error:
    PyErr_SetString(PyExc_OSError, "screen grab failed");

    DeleteDC(screen_copy);
    if (screens == -1) {
        ReleaseDC(wnd, screen);
    } else {
        DeleteDC(screen);
    }

    return NULL;
}

/* -------------------------------------------------------------------- */
/* Windows clipboard grabber */

PyObject *
PyImaging_GrabClipboardWin32(PyObject *self, PyObject *args) {
    HANDLE handle = NULL;
    int size;
    void *data;
    PyObject *result;
    UINT format;
    UINT formats[] = {CF_DIB, CF_DIBV5, CF_HDROP, RegisterClipboardFormatA("PNG"), 0};
    LPCSTR format_names[] = {"DIB", "DIB", "file", "png", NULL};

    if (!OpenClipboard(NULL)) {
        // Maybe the clipboard is temporarily in use by another process.
        // Wait and try again
        Sleep(500);

        if (!OpenClipboard(NULL)) {
            PyErr_SetString(PyExc_OSError, "failed to open clipboard");
            return NULL;
        }
    }

    // find best format as set by clipboard owner
    format = 0;
    while (!handle && (format = EnumClipboardFormats(format))) {
        for (UINT i = 0; formats[i] != 0; i++) {
            if (format == formats[i]) {
                handle = GetClipboardData(format);
                format = i;
                break;
            }
        }
    }

    if (!handle) {
        CloseClipboard();
        return Py_BuildValue("zO", NULL, Py_None);
    }

    data = GlobalLock(handle);
    size = GlobalSize(handle);

    result = PyBytes_FromStringAndSize(data, size);

    GlobalUnlock(handle);
    CloseClipboard();

    if (!result) {
        return NULL;
    }
    return Py_BuildValue("zN", format_names[format], result);
}

/* -------------------------------------------------------------------- */
/* Windows class */

#ifndef WM_MOUSEWHEEL
#define WM_MOUSEWHEEL 522
#endif

static int mainloop = 0;

static void
callback_error(const char *handler) {
    PyObject *sys_stderr;

    sys_stderr = PySys_GetObject("stderr");

    if (sys_stderr) {
        PyFile_WriteString("*** ImageWin: error in ", sys_stderr);
        PyFile_WriteString((char *)handler, sys_stderr);
        PyFile_WriteString(":\n", sys_stderr);
    }

    PyErr_Print();
    PyErr_Clear();
}

static LRESULT CALLBACK
windowCallback(HWND wnd, UINT message, WPARAM wParam, LPARAM lParam) {
    PAINTSTRUCT ps;
    PyObject *callback = NULL;
    PyObject *result;
    PyThreadState *threadstate;
    PyThreadState *current_threadstate;
    HDC dc;
    RECT rect;
    LRESULT status = 0;

    /* set up threadstate for messages that calls back into python */
    switch (message) {
        case WM_CREATE:
            mainloop++;
            break;
        case WM_DESTROY:
            mainloop--;
            /* fall through... */
        case WM_PAINT:
        case WM_SIZE:
            callback = (PyObject *)GetWindowLongPtr(wnd, 0);
            if (callback) {
                threadstate =
                    (PyThreadState *)GetWindowLongPtr(wnd, sizeof(PyObject *));
                current_threadstate = PyThreadState_Swap(NULL);
                PyEval_RestoreThread(threadstate);
            } else {
                return DefWindowProc(wnd, message, wParam, lParam);
            }
    }

    /* process message */
    switch (message) {
        case WM_PAINT:
            /* redraw (part of) window.  this generates a WCK-style
               damage/clear/repair cascade */
            BeginPaint(wnd, &ps);
            dc = GetDC(wnd);
            GetWindowRect(wnd, &rect); /* in screen coordinates */

            result = PyObject_CallFunction(
                callback,
                "siiii",
                "damage",
                ps.rcPaint.left,
                ps.rcPaint.top,
                ps.rcPaint.right,
                ps.rcPaint.bottom
            );
            if (result) {
                Py_DECREF(result);
            } else {
                callback_error("window damage callback");
            }

            result = PyObject_CallFunction(
                callback,
                "s" F_HANDLE "iiii",
                "clear",
                dc,
                0,
                0,
                rect.right - rect.left,
                rect.bottom - rect.top
            );
            if (result) {
                Py_DECREF(result);
            } else {
                callback_error("window clear callback");
            }

            result = PyObject_CallFunction(
                callback,
                "s" F_HANDLE "iiii",
                "repair",
                dc,
                0,
                0,
                rect.right - rect.left,
                rect.bottom - rect.top
            );
            if (result) {
                Py_DECREF(result);
            } else {
                callback_error("window repair callback");
            }

            ReleaseDC(wnd, dc);
            EndPaint(wnd, &ps);
            break;

        case WM_SIZE:
            /* resize window */
            result = PyObject_CallFunction(
                callback, "sii", "resize", LOWORD(lParam), HIWORD(lParam)
            );
            if (result) {
                InvalidateRect(wnd, NULL, 1);
                Py_DECREF(result);
            } else {
                callback_error("window resize callback");
            }
            break;

        case WM_DESTROY:
            /* destroy window */
            result = PyObject_CallFunction(callback, "s", "destroy");
            if (result) {
                Py_DECREF(result);
            } else {
                callback_error("window destroy callback");
            }
            Py_DECREF(callback);
            break;

        default:
            status = DefWindowProc(wnd, message, wParam, lParam);
    }

    if (callback) {
        /* restore thread state */
        PyEval_SaveThread();
        PyThreadState_Swap(current_threadstate);
    }

    return status;
}

PyObject *
PyImaging_CreateWindowWin32(PyObject *self, PyObject *args) {
    HWND wnd;
    WNDCLASS windowClass;

    char *title;
    PyObject *callback;
    int width = 0, height = 0;
    if (!PyArg_ParseTuple(args, "sO|ii", &title, &callback, &width, &height)) {
        return NULL;
    }

    if (width <= 0) {
        width = CW_USEDEFAULT;
    }
    if (height <= 0) {
        height = CW_USEDEFAULT;
    }

    /* register toplevel window class */
    windowClass.style = CS_CLASSDC;
    windowClass.cbClsExtra = 0;
    windowClass.cbWndExtra = sizeof(PyObject *) + sizeof(PyThreadState *);
    windowClass.hInstance = GetModuleHandle(NULL);
    /* windowClass.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1); */
    windowClass.hbrBackground = NULL;
    windowClass.lpszMenuName = NULL;
    windowClass.lpszClassName = "pilWindow";
    windowClass.lpfnWndProc = windowCallback;
    windowClass.hIcon = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(1));
    windowClass.hCursor = LoadCursor(NULL, IDC_ARROW); /* CROSS? */

    RegisterClass(&windowClass); /* FIXME: check return status */

    wnd = CreateWindowEx(
        0,
        windowClass.lpszClassName,
        title,
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        width,
        height,
        HWND_DESKTOP,
        NULL,
        NULL,
        NULL
    );

    if (!wnd) {
        PyErr_SetString(PyExc_OSError, "failed to create window");
        return NULL;
    }

    /* register window callback */
    Py_INCREF(callback);
    SetWindowLongPtr(wnd, 0, (LONG_PTR)callback);
    SetWindowLongPtr(wnd, sizeof(callback), (LONG_PTR)PyThreadState_Get());

    Py_BEGIN_ALLOW_THREADS;
    ShowWindow(wnd, SW_SHOWNORMAL);
    SetForegroundWindow(wnd); /* to make sure it's visible */
    Py_END_ALLOW_THREADS;

    return Py_BuildValue(F_HANDLE, wnd);
}

PyObject *
PyImaging_EventLoopWin32(PyObject *self, PyObject *args) {
    MSG msg;

    Py_BEGIN_ALLOW_THREADS;
    while (mainloop && GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    Py_END_ALLOW_THREADS;

    Py_RETURN_NONE;
}

/* -------------------------------------------------------------------- */
/* windows WMF renderer */

#define GET32(p, o) ((DWORD *)(p + o))[0]

static int CALLBACK
enhMetaFileProc(
    HDC hdc, HANDLETABLE *lpht, const ENHMETARECORD *lpmr, int nHandles, LPARAM data
) {
    PlayEnhMetaFileRecord(hdc, lpht, lpmr, nHandles);
    return 1;
}

PyObject *
PyImaging_DrawWmf(PyObject *self, PyObject *args) {
    HBITMAP bitmap;
    HENHMETAFILE meta;
    BITMAPCOREHEADER core;
    HDC dc;
    RECT rect;
    PyObject *buffer = NULL;
    void *ptr;

    char *data;
    Py_ssize_t datasize;
    int width, height;
    int x0, y0, x1, y1;
    if (!PyArg_ParseTuple(
            args,
            "y#(ii)(iiii):_load",
            &data,
            &datasize,
            &width,
            &height,
            &x0,
            &x1,
            &y0,
            &y1
        )) {
        return NULL;
    }

    /* step 1: copy metafile contents into METAFILE object */

    if (datasize > 22 && GET32(data, 0) == 0x9ac6cdd7) {
        /* placeable windows metafile (22-byte aldus header) */
        meta = SetWinMetaFileBits(datasize - 22, data + 22, NULL, NULL);

    } else if (datasize > 80 && GET32(data, 0) == 1 && GET32(data, 40) == 0x464d4520) {
        /* enhanced metafile */
        meta = SetEnhMetaFileBits(datasize, data);

    } else {
        /* unknown meta format */
        meta = NULL;
    }

    if (!meta) {
        PyErr_SetString(PyExc_OSError, "cannot load metafile");
        return NULL;
    }

    /* step 2: create bitmap */

    core.bcSize = sizeof(core);
    core.bcWidth = width;
    core.bcHeight = height;
    core.bcPlanes = 1;
    core.bcBitCount = 24;

    dc = CreateCompatibleDC(NULL);

    bitmap = CreateDIBSection(dc, (BITMAPINFO *)&core, DIB_RGB_COLORS, &ptr, NULL, 0);

    if (!bitmap) {
        PyErr_SetString(PyExc_OSError, "cannot create bitmap");
        goto error;
    }

    if (!SelectObject(dc, bitmap)) {
        PyErr_SetString(PyExc_OSError, "cannot select bitmap");
        goto error;
    }

    /* step 3: render metafile into bitmap */

    rect.left = rect.top = 0;
    rect.right = width;
    rect.bottom = height;

    /* FIXME: make background transparent? configurable? */
    FillRect(dc, &rect, GetStockObject(WHITE_BRUSH));

    EnumEnhMetaFile(dc, meta, enhMetaFileProc, NULL, &rect);

    /* step 4: extract bits from bitmap */

    GdiFlush();

    buffer = PyBytes_FromStringAndSize(ptr, height * ((width * 3 + 3) & -4));

error:
    DeleteEnhMetaFile(meta);

    if (bitmap) {
        DeleteObject(bitmap);
    }

    DeleteDC(dc);

    return buffer;
}

#endif /* _WIN32 */

/* -------------------------------------------------------------------- */
/* X11 support */

#ifdef HAVE_XCB
#include <xcb/xcb.h>

/* -------------------------------------------------------------------- */
/* X11 screen grabber */

PyObject *
PyImaging_GrabScreenX11(PyObject *self, PyObject *args) {
    int width, height;
    char *display_name;
    xcb_connection_t *connection;
    int screen_number;
    xcb_screen_iterator_t iter;
    xcb_screen_t *screen = NULL;
    xcb_get_image_reply_t *reply;
    xcb_generic_error_t *error;
    PyObject *buffer = NULL;

    if (!PyArg_ParseTuple(args, "|z", &display_name)) {
        return NULL;
    }

    /* connect to X and get screen data */

    connection = xcb_connect(display_name, &screen_number);
    if (xcb_connection_has_error(connection)) {
        PyErr_Format(
            PyExc_OSError,
            "X connection failed: error %i",
            xcb_connection_has_error(connection)
        );
        xcb_disconnect(connection);
        return NULL;
    }

    iter = xcb_setup_roots_iterator(xcb_get_setup(connection));
    for (; iter.rem; --screen_number, xcb_screen_next(&iter)) {
        if (screen_number == 0) {
            screen = iter.data;
            break;
        }
    }
    if (screen == NULL || screen->root == 0) {
        // this case is usually caught with "X connection failed: error 6" above
        xcb_disconnect(connection);
        PyErr_SetString(PyExc_OSError, "X screen not found");
        return NULL;
    }

    width = screen->width_in_pixels;
    height = screen->height_in_pixels;

    /* get image data */

    reply = xcb_get_image_reply(
        connection,
        xcb_get_image(
            connection,
            XCB_IMAGE_FORMAT_Z_PIXMAP,
            screen->root,
            0,
            0,
            width,
            height,
            0x00ffffff
        ),
        &error
    );
    if (reply == NULL) {
        PyErr_Format(
            PyExc_OSError,
            "X get_image failed: error %i (%i, %i, %i)",
            error->error_code,
            error->major_code,
            error->minor_code,
            error->resource_id
        );
        free(error);
        xcb_disconnect(connection);
        return NULL;
    }

    /* store data in Python buffer */

    if (reply->depth == 24) {
        buffer = PyBytes_FromStringAndSize(
            (char *)xcb_get_image_data(reply), xcb_get_image_data_length(reply)
        );
    } else {
        PyErr_Format(PyExc_OSError, "unsupported bit depth: %i", reply->depth);
    }

    free(reply);
    xcb_disconnect(connection);

    if (!buffer) {
        return NULL;
    }

    return Py_BuildValue("(ii)N", width, height, buffer);
}

#endif /* HAVE_XCB */
