gcq
gcq

Reputation: 977

Winapi GetDIBits access violation

I want to get the raw bytes of a BITMAPINFO in python. This is my complete code:

import ctypes
from ctypes import wintypes
windll = ctypes.windll
user32 = windll.user32
gdi32 = windll.gdi32


class RECT(ctypes.Structure):
    _fields_ = [
        ('left', ctypes.c_long),
        ('top', ctypes.c_long),
        ('right', ctypes.c_long),
        ('bottom', ctypes.c_long)
    ]


class BITMAPINFOHEADER(ctypes.Structure):
    _fields_ = [
        ("biSize", wintypes.DWORD),
        ("biWidth", ctypes.c_long),
        ("biHeight", ctypes.c_long),
        ("biPlanes", wintypes.WORD),
        ("biBitCount", wintypes.WORD),
        ("biCompression", wintypes.DWORD),
        ("biSizeImage", wintypes.DWORD),
        ("biXPelsPerMeter", ctypes.c_long),
        ("biYPelsPerMeter", ctypes.c_long),
        ("biClrUsed", wintypes.DWORD),
        ("biClrImportant", wintypes.DWORD)
    ]


class RGBQUAD(ctypes.Structure):
    _fields_ = [
        ("rgbBlue", wintypes.BYTE),
        ("rgbGreen", wintypes.BYTE),
        ("rgbRed", wintypes.BYTE),
        ("rgbReserved", ctypes.c_void_p)
    ]


class BITMAP(ctypes.Structure):
    _fields_ = [
        ("bmType", ctypes.c_long),
        ("bmWidth", ctypes.c_long),
        ("bmHeight", ctypes.c_long),
        ("bmWidthBytes", ctypes.c_long),
        ("bmPlanes", wintypes.DWORD),
        ("bmBitsPixel", wintypes.DWORD),
        ("bmBits", ctypes.c_void_p)
    ]


whandle = 327756  # Just a handle of an open application
rect = RECT()
user32.GetClientRect(whandle, ctypes.byref(rect))
# bbox = (rect.left, rect.top, rect.right, rect.bottom)

hdcScreen = user32.GetDC(None)
hdc = gdi32.CreateCompatibleDC(hdcScreen)
hbmp = gdi32.CreateCompatibleBitmap(
    hdcScreen,
    rect.right - rect.left,
    rect.bottom - rect.top
)
gdi32.SelectObject(hdc, hbmp)

PW_CLIENTONLY = 1

if not user32.PrintWindow(whandle, hdc, PW_CLIENTONLY):
    raise Exception("PrintWindow failed")

bmap = BITMAP()
if not gdi32.GetObjectW(hbmp, ctypes.sizeof(BITMAP), ctypes.byref(bmap)):
    raise Exception("GetObject failed")


class BITMAPINFO(ctypes.Structure):
    _fields_ = [
        ("BITMAPINFOHEADER", BITMAPINFOHEADER),
        ("RGBQUAD", RGBQUAD * 1000)
    ]

bminfo = BITMAPINFO()
bminfo.BITMAPINFOHEADER.biSize = ctypes.sizeof(BITMAPINFOHEADER)
bminfo.BITMAPINFOHEADER.biWidth = bmap.bmWidth
bminfo.BITMAPINFOHEADER.biHeight = bmap.bmHeight
bminfo.BITMAPINFOHEADER.biPlanes = bmap.bmPlanes
bminfo.BITMAPINFOHEADER.biBitCount = bmap.bmBitsPixel
bminfo.BITMAPINFOHEADER.biCompression = 0
bminfo.BITMAPINFOHEADER.biClrImportant = 0

out = ctypes.create_string_buffer(1000)

if not gdi32.GetDIBits(hdc, hbmp, 0, bmap.bmHeight, None, bminfo, 0):
    raise Exception("GetDIBits failed")

I need a way to know how long the array of RGBQUADS has to be in the BITMAPINFO struct and also the lenght of the out buffer. The 1000 is in there as a placeholder.

gdi32.GetDIBits fails with an access violation. I guess it's because i have to have the array and buffer with the correct lenght.

I post the whole source, because i don't know what's failing. Any help is appreciated.

UPDATE

Still getting access violation.

I also saw that there is no RGBQUAD array for 32-bit-per-pixel bitmaps. Is that true?

Upvotes: 0

Views: 1142

Answers (2)

Brian Atwell
Brian Atwell

Reputation: 11

The following code is wrong

def round_up32(n):
    multiple = 32

    while multiple < n:
        multiple += 32

    return multiple

scanline_len = round_up32(bmap.bmWidth * bmap.bmBitsPixel)
data_len = scanline_len * bmap.bmHeight

You are trying to find the number of bytes to create an array or simply allocate a block of memory. Memory is always bytes. So bmap.bmBitsPixel should be converted to bytes. 32 bits is 4 bytes. Since you are already checking bmap.bmBitsPixel for 32 bits replace bmap.bmBitsPixel with 4 and remove round_up32 function.

scanline_len = bmap.bmWidth * 4
data_len = scanline_len * bmap.bmHeight

Upvotes: 1

gcq
gcq

Reputation: 977

What i had wrong:

  • Structs (thanks David) :
    • BITMAP has no DWORDs; it has WORDs.
    • RGBQUADs rgbReserved is a BYTE not a void pointer.
    • BITMAPINFO doesn't need RGBQUAD array for bitmaps with 32 bits per pixel.
  • Pointers (thanks Roger, i come from python :P ):
    • GetDIBits parameter lpvBits needs a pointer to the buffer
    • GetDIBits parameter lpbi needs a pointer to the struct

What i didn't know:

How big the buffer had to be. Quoting Jonathan:

Each row of the bitmap is bmWidth * bmBitsPixel bits in size, rounded up to the next multiple of 32 bits. Multiply the row length by bmHeight to calculate the total size of the image data.

I came up with this:

def round_up32(n):
    multiple = 32

    while multiple < n:
        multiple += 32

    return multiple

scanline_len = round_up32(bmap.bmWidth * bmap.bmBitsPixel)
data_len = scanline_len * bmap.bmHeight

data_len is then used to initalize ctypes.create_string_buffer().

GetDIBits only returns pixel data, so i had to build the header.

After making all this changes nothing failed but the image was inverted. I found that GetDIBits returns the scanlines inverted for compatibilty reasons. I made a new PIL Image from the bytes and then flipped it.

The full source follows:

import struct

from PIL import Image
from PIL.ImageOps import flip

import ctypes
from ctypes import wintypes
windll = ctypes.windll
user32 = windll.user32
gdi32 = windll.gdi32


class RECT(ctypes.Structure):
    _fields_ = [
        ('left', ctypes.c_long),
        ('top', ctypes.c_long),
        ('right', ctypes.c_long),
        ('bottom', ctypes.c_long)
    ]


class BITMAPINFOHEADER(ctypes.Structure):
    _fields_ = [
        ("biSize", wintypes.DWORD),
        ("biWidth", ctypes.c_long),
        ("biHeight", ctypes.c_long),
        ("biPlanes", wintypes.WORD),
        ("biBitCount", wintypes.WORD),
        ("biCompression", wintypes.DWORD),
        ("biSizeImage", wintypes.DWORD),
        ("biXPelsPerMeter", ctypes.c_long),
        ("biYPelsPerMeter", ctypes.c_long),
        ("biClrUsed", wintypes.DWORD),
        ("biClrImportant", wintypes.DWORD)
    ]


class BITMAPINFO(ctypes.Structure):
    _fields_ = [
        ("bmiHeader", BITMAPINFOHEADER)
    ]


class BITMAP(ctypes.Structure):
    _fields_ = [
        ("bmType", ctypes.c_long),
        ("bmWidth", ctypes.c_long),
        ("bmHeight", ctypes.c_long),
        ("bmWidthBytes", ctypes.c_long),
        ("bmPlanes", wintypes.WORD),
        ("bmBitsPixel", wintypes.WORD),
        ("bmBits", ctypes.c_void_p)
    ]


def get_window_image(whandle):
    def round_up32(n):
        multiple = 32

        while multiple < n:
            multiple += 32

        return multiple

    rect = RECT()
    user32.GetClientRect(whandle, ctypes.byref(rect))
    bbox = (rect.left, rect.top, rect.right, rect.bottom)

    hdcScreen = user32.GetDC(None)
    hdc = gdi32.CreateCompatibleDC(hdcScreen)
    hbmp = gdi32.CreateCompatibleBitmap(
        hdcScreen,
        bbox[2] - bbox[0],
        bbox[3] - bbox[1]
    )
    gdi32.SelectObject(hdc, hbmp)

    PW_CLIENTONLY = 1

    if not user32.PrintWindow(whandle, hdc, PW_CLIENTONLY):
        raise Exception("PrintWindow failed")

    bmap = BITMAP()
    if not gdi32.GetObjectW(hbmp, ctypes.sizeof(BITMAP), ctypes.byref(bmap)):
        raise Exception("GetObject failed")

    if bmap.bmBitsPixel != 32:
        raise Exception("WTF")

    scanline_len = round_up32(bmap.bmWidth * bmap.bmBitsPixel)
    data_len = scanline_len * bmap.bmHeight

    # http://msdn.microsoft.com/en-us/library/ms969901.aspx
    bminfo = BITMAPINFO()
    bminfo.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
    bminfo.bmiHeader.biWidth = bmap.bmWidth
    bminfo.bmiHeader.biHeight = bmap.bmHeight
    bminfo.bmiHeader.biPlanes = 1
    bminfo.bmiHeader.biBitCount = 24  # bmap.bmBitsPixel
    bminfo.bmiHeader.biCompression = 0

    data = ctypes.create_string_buffer(data_len)

    DIB_RGB_COLORS = 0

    get_bits_success = gdi32.GetDIBits(
        hdc, hbmp,
        0, bmap.bmHeight,
        ctypes.byref(data), ctypes.byref(bminfo),
        DIB_RGB_COLORS
    )
    if not get_bits_success:
        raise Exception("GetDIBits failed")

    # http://msdn.microsoft.com/en-us/library/dd183376%28v=vs.85%29.aspx
    bmiheader_fmt = "LllHHLLllLL"

    unpacked_header = [
        bminfo.bmiHeader.biSize,
        bminfo.bmiHeader.biWidth,
        bminfo.bmiHeader.biHeight,
        bminfo.bmiHeader.biPlanes,
        bminfo.bmiHeader.biBitCount,
        bminfo.bmiHeader.biCompression,
        bminfo.bmiHeader.biSizeImage,
        bminfo.bmiHeader.biXPelsPerMeter,
        bminfo.bmiHeader.biYPelsPerMeter,
        bminfo.bmiHeader.biClrUsed,
        bminfo.bmiHeader.biClrImportant
    ]

    # Indexes: biXPelsPerMeter = 7, biYPelsPerMeter = 8
    # Value from https://stackoverflow.com/a/23982267/2065904
    unpacked_header[7] = 3779
    unpacked_header[8] = 3779

    image_header = struct.pack(bmiheader_fmt, *unpacked_header)

    image = image_header + data

    return flip(Image.frombytes("RGB", (bmap.bmWidth, bmap.bmHeight), image))

Pass a window handle (int) to get_window_image() and it returns a PIL image.

The only issue is that the colors are... weird? I'll figure that out another time.

Upvotes: 2

Related Questions