Search code examples
pythonscreenshotheader-filesctypesbmp

Creating an image file & header (python + ctypes)


After a lot of back and forth and editing, here's where I am (bear in mind that I'm a noobkin) :

Right now I'm just trying to detect a window and take a screenshot of that window (I will later parse the screenshot for info). I've been extensively googling about ctypes, DLL functions, and taking screenshots in python... I found a few pieces of code that I reused (while searching every function, its args & syntax to make sure I understand everything) but now I'm stumped.

My function works, and my screenshot.bmp file actually has data in it, but I get an error when opening it in Windows... (NOT A VALID BITMAP FILE) :

I believe this is related to the bmp_header or c_bits line but I can't find much... I reviewed the header format and it seems correct to me (windows BITMAPINFOHEADER). The thing I'm unsure of is how we compute the size of the buffer, maybe I did a mistake there ?

I'd really appreciate someone explaining this to me or providing a link to an article that can help me...

Thx a bunch !

#!/usr/bin/env python3

import ctypes
from struct import calcsize, pack
from ctypes.wintypes import RECT, DWORD

EnumWindows = ctypes.windll.user32.EnumWindows
EnumWindowsPointer = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int))
GetWindowText = ctypes.windll.user32.GetWindowTextW
GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW
IsWindowVisible = ctypes.windll.user32.IsWindowVisible
GetWindowRect = ctypes.windll.user32.GetWindowRect
GetWindowDC = ctypes.windll.user32.GetWindowDC
CreateCompatibleDC = ctypes.windll.gdi32.CreateCompatibleDC
CreateCompatibleBitmap = ctypes.windll.gdi32.CreateCompatibleBitmap
SelectObject = ctypes.windll.gdi32.SelectObject
BitBlt = ctypes.windll.gdi32.BitBlt
GetDIBits = ctypes.windll.gdi32.GetDIBits
CreateFile = ctypes.windll.kernel32.CreateFileW
WriteFile = ctypes.windll.kernel32.WriteFile

#Here we find HWND and LPARAM passed by EnumWindows to the callback
def CaptureWindow(hwnd, lParam):
    #Restrict to visible windows
    if IsWindowVisible(hwnd):
        #Get title of the specific window
        length = GetWindowTextLength(hwnd)
        buff = ctypes.create_unicode_buffer(length + 1)
        GetWindowText(hwnd, buff, length + 1)
        #Verify match with our window
        if "(Mode Virtuel)" in buff.value:
            r = ctypes.wintypes.RECT()
            GetWindowRect(hwnd,ctypes.byref(r))
            rWidth = r.right-r.left
            rHeight = r.bottom-r.top
            srcdc = GetWindowDC(hwnd)
            memdc = CreateCompatibleDC(srcdc)
            bmp = CreateCompatibleBitmap(srcdc, rWidth, rHeight)
            SelectObject(memdc, bmp)
            BitBlt(memdc, 0, 0, rWidth, rHeight, srcdc, r.left, r.top, "SRCCOPY")

            #Create a BMP header (Windows BitmapInfoHeader format)
            bmp_header = pack('LLLHHLLLLLL', calcsize('LLLHHLLLLLL'), rWidth, rHeight, 1, 24, 0, 0, 0, 0, 0, 0)
            c_bmp_header = ctypes.c_buffer(bmp_header) 

            #Create buffer for BMP data (H * W * 3 = RGB for each pixel) + (40 bytes for the header)
            c_bits = ctypes.c_buffer(b' ' * ((rHeight * rWidth * 3) + 40))
            GetDIBits(memdc, bmp, 0, rHeight, ctypes.byref(c_bits), ctypes.byref(c_bmp_header), "DIB_RGB_COLORS")

            #Create the file
            screen = CreateFile("screenshot.bmp", 3, 4, 0, 2, 128, 0)
            #Write the contents of my BMP buffer in the file
            c_written = DWORD()
            success = WriteFile(screen, c_bits, len(c_bits), ctypes.byref(c_written), 0)
            #Info
            print("Screenshot saved for : {0}".format(buff.value))
            print("{0} bytes were written...".format(c_written))
    return True

EnumWindows(EnumWindowsPointer(CaptureWindow), 0)

Solution

  • Ok I found a solution but I am disappointed because it doesn't actually solve the problem in my previous code.

    To reply to the comments, I like knowing how things work "under the hood", and I love using ctypes as it teaches me some C and allows me to work with system libraries (which is a very interesting knowledge to have going forward in any coding endeavour I believe). Also I always prefer using native python rather than importing someone else's code (that I don't fully understand). I don't feel like the solution below is fully "my own"....

    Anyway here's my new code which works perfectly and checks my window every 0.5s to check if an action is needed (by parsing a change in color of a specific pixel) :

    #!/usr/bin/env python3
    import time
    import ctypes
    import pyautogui
    from ctypes.wintypes import RECT
    
    EnumWindows = ctypes.windll.user32.EnumWindows
    EnumWindowsPointer = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int))
    GetWindowText = ctypes.windll.user32.GetWindowTextW
    GetWindowTextLength = ctypes.windll.user32.GetWindowTextLengthW
    IsWindowVisible = ctypes.windll.user32.IsWindowVisible
    GetWindowRect = ctypes.windll.user32.GetWindowRect
    SetForegroundWindow = ctypes.windll.user32.SetForegroundWindow
    screenshot = pyautogui.screenshot
    
    #Here we find HWND and LPARAM passed by EnumWindows to the callback
    def CaptureWindow(hwnd, lParam):
        #Restrict to visible windows
        if IsWindowVisible(hwnd):
            #Get title of the specific window
            length = GetWindowTextLength(hwnd)
            buff = ctypes.create_unicode_buffer(length + 1)
            GetWindowText(hwnd, buff, length + 1)
            #Verify match with our window
            if "(Mode Virtuel)" in buff.value:
                #Get dimensions
                r = ctypes.wintypes.RECT()
                GetWindowRect(hwnd,ctypes.byref(r))
                rWidth = r.right-r.left
                rHeight = r.bottom-r.top
                SetForegroundWindow(hwnd)
                screen = screenshot(region=(r.left, r.top, rWidth, rHeight))
                if screen.getpixel((444,412)) == (255, 145, 45):
                    print("Action needed !!!")
                else:
                    print("No action needed !!!")
        return True
    
    
    while True:
        EnumWindows(EnumWindowsPointer(CaptureWindow), 0)
        time.sleep(.5)