-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Allow window ID to be passed to ImageGrab.grab() on macOS #9070
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
Changes from all commits
5ce88db
1f7e9c3
7eaac3f
79914ec
53302c2
610d564
ca3528f
c680ff0
424168b
88247a9
65c32ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -43,17 +43,45 @@ def grab( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fh, filepath = tempfile.mkstemp(".png") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.close(fh) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args = ["screencapture"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if bbox: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if window: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args += ["-l", str(window)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| elif bbox: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| left, top, right, bottom = bbox | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args += ["-R", f"{left},{top},{right-left},{bottom-top}"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| subprocess.call(args + ["-x", filepath]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| im = Image.open(filepath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| im.load() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.unlink(filepath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if bbox: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| im_resized = im.resize((right - left, bottom - top)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| im.close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return im_resized | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if window: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Determine if the window was in Retina mode or not | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # by capturing it without the shadow, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # and checking how different the width is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fh, filepath = tempfile.mkstemp(".png") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.close(fh) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| subprocess.call( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["screencapture", "-l", str(window), "-o", "-x", filepath] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with Image.open(filepath) as im_no_shadow: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| retina = im.width - im_no_shadow.width > 100 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| retina = im.width - im_no_shadow.width > 100 | |
| # Detect Retina mode by checking if the width difference is >30% of the no-shadow image width | |
| retina = (im.width - im_no_shadow.width) / im_no_shadow.width > 0.3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current comment is
Determine if the window was in Retina mode or not by capturing it without the shadow, and checking how different the width is
I think it's reasonably apparent that we're using an absolute threshold
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When both window and bbox are provided, the window is captured 3 times:
- Line 51: Initial capture with shadow
- Line 62-63: Capture without shadow for Retina detection
- Implicitly through the subprocess calls
This is inefficient and could impact performance, especially for larger windows. Consider whether the Retina detection step is necessary, or if there's a way to determine Retina mode without an additional capture.
| # Determine if the window was in Retina mode or not | |
| # by capturing it without the shadow, | |
| # and checking how different the width is | |
| fh, filepath = tempfile.mkstemp(".png") | |
| os.close(fh) | |
| subprocess.call( | |
| ["screencapture", "-l", str(window), "-o", "-x", filepath] | |
| ) | |
| with Image.open(filepath) as im_no_shadow: | |
| retina = im.width - im_no_shadow.width > 100 | |
| os.unlink(filepath) | |
| # Since screencapture's -R does not work with -l, | |
| # crop the image manually | |
| if retina: | |
| left, top, right, bottom = bbox | |
| im_cropped = im.resize( | |
| (right - left, bottom - top), | |
| # Use the first captured image's dimensions to infer Retina mode. | |
| # If the image width is roughly double the bbox width, it's Retina. | |
| left, top, right, bottom = bbox | |
| bbox_width = right - left | |
| retina = im.width >= bbox_width * 2 | |
| # Since screencapture's -R does not work with -l, | |
| # crop the image manually | |
| if retina: | |
| im_cropped = im.resize( | |
| (bbox_width, bottom - top), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The window is captured twice.
Implicitly through the subprocess calls
This isn't an additional time.
Consider whether the Retina detection step is necessary, or if there's a way to determine Retina mode without an additional capture.
See discussion at #9070 (comment)
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] In Retina mode, the code uses resize() with a box parameter to crop. However, resize() is primarily intended for scaling, not cropping. While this works (the box parameter specifies the region to sample from the source), it's unconventional and potentially confusing.
Consider using a two-step approach for clarity:
if retina:
left, top, right, bottom = bbox
im_cropped = im.crop(tuple(coord * 2 for coord in bbox))
im_cropped = im_cropped.resize((right - left, bottom - top))Or document why resize() with box is used instead of the more idiomatic crop() + resize() combination.
| im_cropped = im.resize( | |
| (right - left, bottom - top), | |
| box=tuple(coord * 2 for coord in bbox), | |
| ) | |
| im_cropped = im.crop(tuple(coord * 2 for coord in bbox)) | |
| im_cropped = im_cropped.resize((right - left, bottom - top)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling two methods would not be more efficient, from a performance perspective, than one.
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new macOS window capture functionality lacks test coverage. While Tests/test_imagegrab.py includes a test_grab_invalid_handle test for Windows window capture, there are no corresponding tests for macOS window capture scenarios.
Consider adding tests for:
- Basic window capture on macOS (
ImageGrab.grab(window=<valid_window_id>)) - Window capture with bbox on macOS
- Invalid window IDs on macOS
This is especially important given the complexity of the Retina detection logic introduced in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've created #9321
Copilot
AI
Nov 30, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The variable name im_cropped is used here, while a similar operation at line 82 uses im_resized. For consistency and accuracy, consider using im_cropped in both places since both operations effectively crop/extract a region from the original image (one uses crop() and one uses resize() with a box parameter, but both achieve cropping).
| im_resized = im.resize((right - left, bottom - top)) | |
| im.close() | |
| return im_resized | |
| im_cropped = im.resize((right - left, bottom - top)) | |
| im.close() | |
| return im_cropped |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a resize operation, not a crop operation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
subprocess.call()at lines 51, 62-63 don't check the return code. Ifscreencapturefails (e.g., invalid window ID, permission denied), the code will proceed to try opening a potentially non-existent or corrupt file, which could raise an unexpected exception.Consider checking the return code and raising an appropriate error:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've created #9321