Winapi GetDIBits 访问冲突
我想在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
中的DWORD
为WORD
,并将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 个回答
下面的代码是错误的
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
我之前搞错了什么:
- 结构体(感谢David):
BITMAP
里面没有DWORD
,它里面有WORD
。RGBQUAD
里的rgbReserved
是BYTE
,而不是空指针。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图像。
唯一的问题是颜色有点... 奇怪? 这个我下次再解决。