Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ImageGrab.grab() doesn't ignore windows layered on top (tooltip, pop-ups, etc.) #8456

Open
a-alak opened this issue Oct 10, 2024 · 20 comments
Open

Comments

@a-alak
Copy link

a-alak commented Oct 10, 2024

What did you do?

Run PIL.ImageGrab.grab() without arguments.

What did you expect to happen?

Until now this has not included pop-ups and tooltips in the screenshot.
I just updated to windows 11.
I can see in issue #2569 that it is the expected behavior to not include layered windows, and that the option to include them was added, but not as a default.

I suppose the underlying API has changed in windows 11.
Anyone that can share some knowledge about this?

What are your OS, Python and Pillow versions?

  • OS: Windows 11
  • Python: 3.10.5
  • Pillow: 10.4.0
--------------------------------------------------------------------
Pillow 10.4.0
Python 3.10.5 (tags/v3.10.5:f377153, Jun  6 2022, 16:14:13) [MSC v.1929 64 bit (AMD64)]
--------------------------------------------------------------------
Python executable is C:\Users\Public\SPEDA\env\Scripts\python.exe
Environment Python files loaded from C:\Users\Public\SPEDA\env
System Python files loaded from C:\Program Files\Python310
--------------------------------------------------------------------
Python Pillow modules loaded from C:\Users\Public\SPEDA\env\lib\site-packages\PIL
Binary Pillow modules loaded from C:\Users\Public\SPEDA\env\lib\site-packages\PIL
--------------------------------------------------------------------
--- PIL CORE support ok, compiled for 10.4.0
--- TKINTER support ok, loaded 8.6
--- FREETYPE2 support ok, loaded 2.13.2
--- LITTLECMS2 support ok, loaded 2.16
--- WEBP support ok, loaded 1.4.0
--- WEBP Transparency support ok
--- WEBPMUX support ok
--- WEBP Animation support ok
--- JPEG support ok, compiled for libjpeg-turbo 3.0.3
--- OPENJPEG (JPEG2000) support ok, loaded 2.5.2
--- ZLIB (PNG/ZIP) support ok, loaded 1.3.1
--- LIBTIFF support ok, loaded 4.6.0
*** RAQM (Bidirectional Text) support not installed
*** LIBIMAGEQUANT (Quantization method) support not installed
*** XCB (X protocol) support not installed
--------------------------------------------------------------------
@radarhere radarhere changed the title PIL.ImageGrab.grab() doesn't ignore windows layered on top (tooltip, pop-ups, etc.) ImageGrab.grab() doesn't ignore windows layered on top (tooltip, pop-ups, etc.) Oct 10, 2024
@radarhere
Copy link
Member

Could you attach an image to show what you're seeing?

@a-alak
Copy link
Author

a-alak commented Oct 10, 2024

Yeah sorry, what i tested on was sensitive data, so i could not share.
But here is an alternative example with the file explorer.
Example 1 shows a tooltip with the url of the Outlook shortcut.
Usually i would have expected the tooltip not to show in the screenshot like example 2.

exmple1
example2

@radarhere
Copy link
Member

@nulano did you have any insight here?

@nulano
Copy link
Contributor

nulano commented Oct 21, 2024

I can confirm that tooltips are included with both include_layered_windows=False and include_layered_windows=True on Win 11 Pro version 23H2, but also on Win 10 Home version 22H2.

I just updated to windows 11.

Do you know which build of Win 10 you were running before you upgraded?

@a-alak
Copy link
Author

a-alak commented Oct 23, 2024

Unfortunately not.
And honestly I am more challenged by popups than tool tips, and I cannot show you an example pic of the pop up.
It is running on a hospital system, where there often is weird pop up messages. They did not use to show up in ImageGrab, but now they do.

@radarhere
Copy link
Member

Do these popups come from the same application that you are trying to see in your screenshot, or a different one? I'm wondering if the request of #4415 could be a potential solution.

@a-alak
Copy link
Author

a-alak commented Oct 29, 2024

I think that might solve it!
It is popups from another application.
Some kind of background internal messaging service, that I can't disable.

@radarhere
Copy link
Member

I've created #8516 to resolve this by adding a handle argument to ImageGrab.grab() that accepts a HDC.

With that, you could do something like

import win32gui
from PIL import ImageGrab
window = win32gui.FindWindow(None, "Insert window title here")
handle = win32gui.GetDC(window)
ImageGrab.grab(handle=handle)

@a-alak
Copy link
Author

a-alak commented Oct 31, 2024

Amazing! Thank you @radarhere!
Any idea when you can expect a release including PR #8516, how does pillow release cycles work?
Sorry if the question is stupid.

@radarhere
Copy link
Member

Pillow releases occur every three months - the next one is scheduled for January 2nd.

If you would like to try out the PR in the meantime, I've put together a wheel - pillow-11.1.0.dev0-cp310-cp310-win_amd64.whl.zip

@a-alak
Copy link
Author

a-alak commented Nov 3, 2024

Amazing! Thanks @radarhere!

I am curious whether the tooltip issue can be solved as well?
It is a minor problem for us, but I am just going to leave the issue open for that.
If it is not possible or not something that people want in pillow I will close the issue 😇

@radarhere
Copy link
Member

I expect it is something that people would want - I'm just not convinced that Windows makes that feature available.

https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-bitblt is the API that we use to get the screenshot, and CAPTUREBLT is the option that you can turn on and off with include_layered_windows that "Includes any windows that are layered on top". Nothing else on that page seems relevant.

#8456 (comment) was our attempt to test if there was a change between Windows 10 and 11, and we didn't find any difference.

@a-alak
Copy link
Author

a-alak commented Nov 7, 2024

Thanks for sharing!
I just tested out the wheel you send and I could not make it work.
The screenshot is just a black screen. I run the exact same code you wrote on win 11 python 3.10.

@nulano
Copy link
Contributor

nulano commented Nov 7, 2024

I just tested out the wheel you send and I could not make it work.
The screenshot is just a black screen. I run the exact same code you wrote on win 11 python 3.10.

Yep, that unfortunately matches what I found also, some applications don't seem to be able to be captured in this way: #8516 (review)

(worth noting that instead of using win32gui I used Microsoft Spy++; I saw that some applications have multiple windows and can only be captured by the outer-most one, but that is the one that has a title so it should also be selected by win32gui)

@radarhere
Copy link
Member

I wonder if there's another way to solve your problem of these popups covering your application - by moving your application to the foreground? https://stackoverflow.com/questions/66164926/in-python-how-do-i-make-a-specific-window-stay-on-top

@a-alak
Copy link
Author

a-alak commented Nov 8, 2024

Unfortunately the pop-up is somehow forced as foreground, so even though I try to pull target window to the front, it will not work.

Anyway, my colleague wrote this short script that works and does not have a black window:

import time
import ctypes
import win32gui
import win32ui
import win32con
from PIL import Image

# Window Title
window_title = 'SOME TITLE'

# Find the window by title
hwnd = win32gui.FindWindow(None, window_title)

if hwnd:
    # Get the window's dimensions
    left, top, right, bottom = win32gui.GetWindowRect(hwnd)
    width = right - left
    height = bottom - top

    # Get the window's device context (DC)
    window_dc = win32gui.GetWindowDC(hwnd)
    mfc_dc = win32ui.CreateDCFromHandle(window_dc)
    save_dc = mfc_dc.CreateCompatibleDC()

    # Create a bitmap object
    bitmap = win32ui.CreateBitmap()
    bitmap.CreateCompatibleBitmap(mfc_dc, width, height)
    save_dc.SelectObject(bitmap)

    # Use ctypes to call PrintWindow
    PW_RENDERFULLCONTENT = 2
    result = ctypes.windll.user32.PrintWindow(hwnd, save_dc.GetSafeHdc(), PW_RENDERFULLCONTENT)

    if result == 1:
        # Save the bitmap to a file
        bitmap.SaveBitmapFile(save_dc, 'screenshot.bmp')

        # Convert the bitmap to a PIL image and save as PNG
        bmp_info = bitmap.GetInfo()
        bmp_str = bitmap.GetBitmapBits(True)
        img = Image.frombuffer(
            'RGB',
            (bmp_info['bmWidth'], bmp_info['bmHeight']),
            bmp_str, 'raw', 'BGRX', 0, 1
        )
        img.save('screenshot.png')

        # Display the screenshot
        img.show()
    else:
        print("Failed to capture the window content.")

    # Clean up
    win32gui.DeleteObject(bitmap.GetHandle())
    save_dc.DeleteDC()
    mfc_dc.DeleteDC()
    win32gui.ReleaseDC(hwnd, window_dc)
else:
    print(f"No window found with title: {window_title}") 

I have a hard time figuring out what the difference between your C implementation and the above implementation is, but maybe this could help figuring out the reason for the black screens?

@nulano
Copy link
Contributor

nulano commented Nov 8, 2024

The difference is almost certainly the use of the PrintWindow function instead of BitBlt. But I cannot easily say how compatible that is with various programs IIUC BitBlt copies the (already painted) window data from the window manager buffer, and PrintWindow sends a message to the window requesting that it paint itself into a provided buffer; or at least that is what the documentation suggests, but it is possible that it is slightly outdated by now.

@radarhere
Copy link
Member

The screenshot is just a black screen.

The black screen is the right size for the window though, yes?

Something that may or may not be related to your initial report that Windows 10 and Windows 11 behave differently - https://stackoverflow.com/a/54572219/4093019

this only occurs with the Windows update 1809 from late 2018. Apparently, Windows changed the way it handles clipping with that update so that the Device Context contents are no longer updated for parts of windows that are located offscreen.

But I expect you'll tell me that the window you're trying to capture is always entirely on-screen?

@nulano
Copy link
Contributor

nulano commented Nov 9, 2024

But I expect you'll tell me that the window you're trying to capture is always entirely on-screen?

That was what I observed, yes. A black box of the correct size while capturing a window that is visible and focused.

@radarhere
Copy link
Member

If both BitBlt and PrintWindow are flawed, then my next idea would be for Pillow to run both when capturing a window - if one is non-black, then return that to the user. Otherwise just return the BitBlt image (it is possible the window is actually black).

Does that sound like a good idea, or it is too expensive/convoluted?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants