Search code examples
python-3.xpython-imaging-libraryscreenshotdpiwin32gui

How to get screenshot and change DPI on the clipboard?


Under Win7 I would like to get the content of a window on the clipboard and set/adjust the DPI setting on the clipboard and copy it to a final application.

The MCVE below is not yet working as desired. There is an issue: sometimes it can happen that apparently the window is not yet set to foreground and the ImageGrab.grab(bbox) gets the wrong content. Waiting for some time (2-5 sec) helps, but is not very practical. How to avoid or workaround this?

Here is the code:

from io import BytesIO
from PIL import Image,ImageGrab
import win32gui, win32clipboard
import time

def get_screenshot(window_name, dpi):
    hwnd = win32gui.FindWindow(None, window_name)
    if hwnd != 0:
        win32gui.SetForegroundWindow(hwnd)
        time.sleep(2)  ### sometimes window is not yet in foreground. delay/timing problem???
        bbox = win32gui.GetWindowRect(hwnd)
        screenshot = ImageGrab.grab(bbox)
        width, height = screenshot.size
        lmargin = 9
        tmargin = 70
        rmargin = 9
        bmargin = 36
        screenshot = screenshot.crop(box = (lmargin,tmargin,width-rmargin,height-bmargin))
        win32clipboard.OpenClipboard()
        win32clipboard.EmptyClipboard()
        output = BytesIO()
        screenshot.convert("RGB").save(output, "BMP", dpi=(dpi,dpi))
        data = output.getvalue()[14:]
        output.close()
        win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
        win32clipboard.CloseClipboard()
        print("Screenshot taken...")
    else:
        print("No window found named:", window_name)

window_name = "Gnuplot (window id : 0)"
get_screenshot(window_name,200)

Edit:

also this attempt to improve still gets sometimes the wrong content. Maybe somebody can explain why?

win32gui.SetForegroundWindow(hwnd)
for i in range(1000):
    print(i)
    time.sleep(0.01)
    if win32gui.GetForegroundWindow() == hwnd:
        break
bbox = win32gui.GetWindowRect(hwnd)

Addition:

That's what I (typically) get when I remove the line with the delay time time.sleep(2).

Left: desired content, right: received content. How can I get a reliable capture of content the desired window? What's wrong with the code? The larger I set the delay time the higher the probability that I get the desired content. But I don't want to wait several seconds to be sure. How can I check whether the system is ready for a screenshot?

enter image description here


Solution

  • Thanks to @Tarun Lalwani pointing to this answer, I finally have a code which is working for me for the time being. However, it seems to me quite lengthy with a lot of different modules. Maybe it still can be simplified. Suggestions are welcome.

    Code:

    ### get the content of a window and crop it
    import win32gui, win32ui, win32clipboard
    from io import BytesIO
    from ctypes import windll
    from PIL import Image
    
    # user input
    window_name = 'Gnuplot (window id : 0)'
    margins = [8,63,8,31]    # left, top, right, bottom
    dpi = 96
    
    hwnd = win32gui.FindWindow(None, window_name)
    left, top, right, bottom = win32gui.GetWindowRect(hwnd)
    width = right - left 
    height = bottom - top
    crop_box = (margins[0],margins[1],width-margins[2],height-margins[3])
    
    hwndDC = win32gui.GetWindowDC(hwnd)
    mfcDC  = win32ui.CreateDCFromHandle(hwndDC)
    saveDC = mfcDC.CreateCompatibleDC()
    
    saveBitMap = win32ui.CreateBitmap()
    saveBitMap.CreateCompatibleBitmap(mfcDC, width, height)
    saveDC.SelectObject(saveBitMap)
    result = windll.user32.PrintWindow(hwnd, saveDC.GetSafeHdc(), 0)
    bmpinfo = saveBitMap.GetInfo()
    bmpstr = saveBitMap.GetBitmapBits(True)
    
    im = Image.frombuffer( 'RGB', (bmpinfo['bmWidth'], bmpinfo['bmHeight']),
        bmpstr, 'raw', 'BGRX', 0, 1).crop(crop_box)
    win32clipboard.OpenClipboard()
    win32clipboard.EmptyClipboard()
    output = BytesIO()
    im.convert("RGB").save(output, "BMP", dpi=(dpi,dpi))
    data = output.getvalue()[14:]
    output.close()
    win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
    win32clipboard.CloseClipboard()
    
    win32gui.DeleteObject(saveBitMap.GetHandle())
    saveDC.DeleteDC()
    mfcDC.DeleteDC()
    win32gui.ReleaseDC(hwnd, hwndDC)
    
    print('"'+window_name+'"', "is now on the clipboard with", dpi, "dpi.")
    ### end of code