Winapi GetDIBits 访问冲突

0 投票
2 回答
1067 浏览
提问于 2025-04-18 11:15

我想在Python中获取一个BITMAPINFO的原始字节。这是我的完整代码:

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")

我需要知道BITMAPINFO结构中RGBQUADS数组的长度,以及out缓冲区的长度。这里的1000只是一个占位符。

gdi32.GetDIBits出现了访问违规的错误。我猜是因为我需要确保数组和缓冲区的长度是正确的。

我发布了整个源代码,因为我不知道哪里出错了。任何帮助都非常感谢。

更新

  • 修正了BITMAP中的DWORDWORD,并将RGBQUAD中的空指针改为BYTE
  • 获取图像数据的大小:

    def round_up32(n):
        multiple = 32
    
        while multiple < n:
            multiple += 32
    
        return multiple
    
    data_len = round_up32(bmap.bmWidth * bmap.bmBitsPixel) * bmap.bmHeight
    

仍然出现访问违规的错误。

我还看到对于每像素32位的位图没有RGBQUAD数组。这是真的吗?

2 个回答

0

下面的代码是错误的

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

你正在尝试计算创建一个数组所需的字节数,或者简单来说,就是分配一块内存。内存的单位总是字节。所以 bmap.bmBitsPixel 这个值应该转换成字节。32位等于4字节。因为你已经在检查 bmap.bmBitsPixel 是否为32位,所以可以把 bmap.bmBitsPixel 替换成4,并且去掉 round_up32 这个函数。

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

我之前搞错了什么:

  • 结构体(感谢David):
    • BITMAP里面没有DWORD,它里面有WORD
    • RGBQUAD里的rgbReservedBYTE,而不是空指针。
    • BITMAPINFO对于每个像素32位的位图,不需要RGBQUAD数组。
  • 指针(感谢Roger,我是从Python过来的 :P):
    • GetDIBits的参数lpvBits需要指向缓冲区的指针。
    • GetDIBits的参数lpbi需要指向结构体的指针。

我不知道的事情:

缓冲区需要多大。引用Jonathan的话:

位图的每一行大小是 bmWidth * bmBitsPixel 位,向上取整到下一个32位的倍数。将行长度乘以 bmHeight 就可以计算出图像数据的总大小。

我得出了这个:

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然后用来初始化ctypes.create_string_buffer()

GetDIBits只返回像素数据,所以我还得自己构建头部。

做完这些修改后,虽然没有出错,但图像是倒过来的。我发现GetDIBits为了兼容性原因返回的扫描线是倒的。我用这些字节创建了一个新的PIL图像,然后把它翻转过来了。

完整的源代码如下:

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))

传一个窗口句柄(整数)给get_window_image(),它会返回一个PIL图像。

唯一的问题是颜色有点... 奇怪? 这个我下次再解决。

撰写回答