← Back to Workflow
Internal

capture_devtools_bg.py

Python script that captures a screenshot of the Dev Tools Chrome browser window without bringing it to the foreground, using the Win32 PrintWindow API. Identifies the correct window by matching chrome.exe processes that were launched with the chrome-devtools-profile flag.

"""
Capture a screenshot of the Dev Tools Chrome browser window WITHOUT bringing it
to the foreground, using the Win32 PrintWindow API.
Usage: python capture_devtools_bg.py [output_path]
"""
import ctypes
import ctypes.wintypes
import subprocess
import sys

# We need win32 modules
try:
    import win32gui
    import win32ui
    import win32con
    HAS_WIN32 = True
except ImportError:
    HAS_WIN32 = False

from PIL import Image


def get_devtools_pids():
    """Get all PIDs of chrome.exe processes using the chrome-devtools-profile."""
    result = subprocess.run(
        'wmic process where "name=\'chrome.exe\' and commandline like \'%chrome-devtools-profile%\'" get processid',
        shell=True, capture_output=True, text=True
    )
    pids = set()
    for line in result.stdout.splitlines():
        line = line.strip()
        if line.isdigit():
            pids.add(int(line))
    return pids


def find_main_chrome_window(pids):
    """Find the main Chrome window HWND from the set of PIDs."""
    GetWindowThreadProcessId = ctypes.windll.user32.GetWindowThreadProcessId
    IsWindowVisible = ctypes.windll.user32.IsWindowVisible
    GetWindowTextW = ctypes.windll.user32.GetWindowTextW
    GetWindowTextLengthW = ctypes.windll.user32.GetWindowTextLengthW
    EnumWindows = ctypes.windll.user32.EnumWindows
    WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM)

    candidates = []

    def callback(hwnd, _):
        if not IsWindowVisible(hwnd):
            return True
        pid = ctypes.wintypes.DWORD()
        GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
        if pid.value in pids:
            length = GetWindowTextLengthW(hwnd)
            if length > 0:
                buf = ctypes.create_unicode_buffer(length + 1)
                GetWindowTextW(hwnd, buf, length + 1)
                title = buf.value
                # Skip small dialog/notification windows; keep the main browser
                if title and 'wants to' not in title:
                    candidates.append((hwnd, title))
        return True

    EnumWindows(WNDENUMPROC(callback), 0)
    if candidates:
        # Prefer the one with the longest title (usually the main content window)
        candidates.sort(key=lambda x: len(x[1]), reverse=True)
        return candidates[0]
    return None, None


def capture_window_background_pil(hwnd, output_path):
    """Capture window using PIL ImageGrab with GetWindowRect (no foreground needed
    if window is visible, but won't capture if occluded)."""
    rect = ctypes.wintypes.RECT()
    ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect))
    from PIL import ImageGrab
    img = ImageGrab.grab(bbox=(rect.left, rect.top, rect.right, rect.bottom))
    img.save(output_path)
    return True


def capture_window_background_win32(hwnd, output_path):
    """Capture window content using PrintWindow - works even if window is behind others."""
    rect = ctypes.wintypes.RECT()
    ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect))
    width = rect.right - rect.left
    height = rect.bottom - rect.top

    hwndDC = win32gui.GetWindowDC(hwnd)
    mfcDC = win32ui.CreateDCFromHandle(hwndDC)
    saveDC = mfcDC.CreateCompatibleDC()

    saveBitMap = win32ui.CreateBitmap()
    saveBitMap.CreateCompatibleBitmap(mfcDC, width, height)
    saveDC.SelectObject(saveBitMap)

    # PW_RENDERFULLCONTENT = 2 captures even DWM-composited content
    result = ctypes.windll.user32.PrintWindow(hwnd, saveDC.GetSafeHdc(), 2)

    bmpinfo = saveBitMap.GetInfo()
    bmpstr = saveBitMap.GetBitmapBits(True)

    img = Image.frombuffer('RGB', (bmpinfo['bmWidth'], bmpinfo['bmHeight']),
                           bmpstr, 'raw', 'BGRX', 0, 1)
    img.save(output_path)

    win32gui.DeleteObject(saveBitMap.GetHandle())
    saveDC.DeleteDC()
    mfcDC.DeleteDC()
    win32gui.ReleaseDC(hwnd, hwndDC)
    return True


if __name__ == '__main__':
    output = sys.argv[1] if len(sys.argv) > 1 else 'devtools_screenshot.png'

    pids = get_devtools_pids()
    if not pids:
        print("ERROR: No chrome-devtools-profile processes found.")
        sys.exit(1)
    print(f"Found {len(pids)} devtools PIDs")

    hwnd, title = find_main_chrome_window(pids)
    if not hwnd:
        print("ERROR: Could not find Dev Tools Chrome window.")
        sys.exit(1)
    print(f"Found window: '{title}' (HWND {hwnd})")

    if HAS_WIN32:
        print("Using PrintWindow (background capture)...")
        capture_window_background_win32(hwnd, output)
    else:
        print("win32gui not available, falling back to PIL grab (may capture foreground)...")
        capture_window_background_pil(hwnd, output)

    print(f"Saved to: {output}")

This is used in: