parent
d145cec21d
commit
0997006ac3
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,21 @@
|
|||||||
|
Desktopmagic license:
|
||||||
|
|
||||||
|
Copyright (c) 2011 Ivan Kozik
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
@ -0,0 +1,21 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: Desktopmagic
|
||||||
|
Version: 14.3.11
|
||||||
|
Summary: Robust multi-monitor screenshot grabber (Windows-only right now)
|
||||||
|
Home-page: https://github.com/ludios/Desktopmagic
|
||||||
|
Author: Ivan Kozik
|
||||||
|
Author-email: ivan@ludios.org
|
||||||
|
License: UNKNOWN
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Programming Language :: Python :: 2
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Environment :: Win32 (MS Windows)
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
|
Classifier: Topic :: Multimedia :: Graphics :: Capture :: Screen Capture
|
||||||
|
|
||||||
|
UNKNOWN
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
|||||||
|
../../Scripts/screengrab_torture_test,sha256=M0lDv6izfH8QGmYx6xFoeZVTv_Jx9nLVHh0Ci0YkjPc,80
|
||||||
|
Desktopmagic-14.3.11.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
Desktopmagic-14.3.11.dist-info/LICENSE.txt,sha256=81jpSYGoAoxvlRjLO2NTfL0AHHyDJAMMHSCRURRATKo,1077
|
||||||
|
Desktopmagic-14.3.11.dist-info/METADATA,sha256=nt1JjqpQb_fvEJac3LbQY1AONRJEVtX3gs6zM2dT4XI,696
|
||||||
|
Desktopmagic-14.3.11.dist-info/RECORD,,
|
||||||
|
Desktopmagic-14.3.11.dist-info/WHEEL,sha256=JtBte-IW7C3UcYx3ZpZORq-KtnjVj4xdM4AJCTZPivc,98
|
||||||
|
Desktopmagic-14.3.11.dist-info/top_level.txt,sha256=Y3lWkebSjPOAXP7mzfuDAlkutR0_QwhHEENcXjARMOI,13
|
||||||
|
desktopmagic/__init__.py,sha256=DO7UEI9j-9pPANCC0gBBxPxTTRTd11-vYhlURkvGqFM,24
|
||||||
|
desktopmagic/__pycache__/__init__.cpython-37.pyc,,
|
||||||
|
desktopmagic/__pycache__/screengrab_win32.cpython-37.pyc,,
|
||||||
|
desktopmagic/screengrab_win32.py,sha256=-lu2xdSdAWnWFDJMjxfpIcRiqc78TRGC1-XrFFhery4,15899
|
||||||
|
desktopmagic/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
desktopmagic/scripts/__pycache__/__init__.cpython-37.pyc,,
|
||||||
|
desktopmagic/scripts/__pycache__/screengrab_torture_test.cpython-37.pyc,,
|
||||||
|
desktopmagic/scripts/__pycache__/screengrab_watch_display_rect.cpython-37.pyc,,
|
||||||
|
desktopmagic/scripts/__pycache__/screengrab_watch_virtual_screen_rect.cpython-37.pyc,,
|
||||||
|
desktopmagic/scripts/screengrab_torture_test.py,sha256=-MpUliKrDSZdx_0gutvK7dslrRT7orbvhfbjy-LqIug,1209
|
||||||
|
desktopmagic/scripts/screengrab_watch_display_rect.py,sha256=AhFxIHaCUnL755ds4RoaxhNknbfdzWnLz_3n-fmjhF4,682
|
||||||
|
desktopmagic/scripts/screengrab_watch_virtual_screen_rect.py,sha256=yDhmeajPPnV8a2WVvJoVnv6l8DUQpAMubOZfUPwQCaU,697
|
||||||
|
desktopmagic/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
desktopmagic/test/__pycache__/__init__.cpython-37.pyc,,
|
||||||
|
desktopmagic/test/__pycache__/test_screengrab_win32.cpython-37.pyc,,
|
||||||
|
desktopmagic/test/test_screengrab_win32.py,sha256=vWdtKxFD1CvudkuTeSgoiZwiqTNfgcMW9eq7KjMddjg,3652
|
@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.33.1)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: cp37-none-any
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
desktopmagic
|
@ -0,0 +1 @@
|
|||||||
|
__version__ = '14.3.11'
|
@ -0,0 +1,494 @@
|
|||||||
|
"""
|
||||||
|
Robust functions for grabbing and saving screenshots on Windows.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import win32gui
|
||||||
|
import win32ui
|
||||||
|
import win32con
|
||||||
|
import win32api
|
||||||
|
|
||||||
|
try:
|
||||||
|
long
|
||||||
|
except NameError:
|
||||||
|
# Python 3
|
||||||
|
long = int
|
||||||
|
|
||||||
|
|
||||||
|
def checkRect(rect):
|
||||||
|
"""
|
||||||
|
Check C{rect} for validity.
|
||||||
|
|
||||||
|
Raises L{ValueError} if C{rect}'s computed width or height is zero or
|
||||||
|
negative, or if rect contains nonsense.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
left, top, right, bottom = rect
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("%r is not a valid rect; must contain 4 ints" % (rect,))
|
||||||
|
if not all(isinstance(x, (int, long)) for x in rect):
|
||||||
|
raise ValueError("%r is not a valid rect; must contain 4 ints" % (rect,))
|
||||||
|
width = right - left
|
||||||
|
height = bottom - top
|
||||||
|
if width <= 0 or height <= 0:
|
||||||
|
raise ValueError("%r is not a valid rect; width and height must not be "
|
||||||
|
"zero or negative" % (rect,))
|
||||||
|
|
||||||
|
|
||||||
|
class RectFailed(Exception):
|
||||||
|
"""
|
||||||
|
Could not get information about the virtual screen or a display.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def getVirtualScreenRect():
|
||||||
|
"""
|
||||||
|
Returns a tuple containing (
|
||||||
|
the x-coordinate of the upper-left corner of the virtual screen,
|
||||||
|
the y-coordinate of the upper-left corner of the virtual screen,
|
||||||
|
the x-coordinate of the lower-right corner of the virtual screen,
|
||||||
|
the y-coordinate of the lower-right corner of the virtual screen
|
||||||
|
)
|
||||||
|
|
||||||
|
Note that both x and y coordinates may be negative; the (0, 0) origin is
|
||||||
|
determined by the top-left corner of the main display (not necessarily
|
||||||
|
Display 1).
|
||||||
|
|
||||||
|
Internally, this grabs the geometry from Windows at least twice to avoid
|
||||||
|
getting bad geometry during changes to the display configuration.
|
||||||
|
|
||||||
|
Raises L{RectFailed} if the geometry cannot be retrieved, though
|
||||||
|
this failure mode has never been observed.
|
||||||
|
"""
|
||||||
|
# Note that one iteration of the loop takes about 2us on a Q6600.
|
||||||
|
tries = 150
|
||||||
|
lastRect = None
|
||||||
|
for _ in range(tries):
|
||||||
|
# Get dimensions of the entire virtual screen. Note that left/top may be negative.
|
||||||
|
# Any of these may return nonsense numbers during display configuration
|
||||||
|
# changes (not just "desync" between our calls, but numbers that make little
|
||||||
|
# sense, as if some Windows state doesn't change synchronously.)
|
||||||
|
# This is why we get them at least twice.
|
||||||
|
left = win32api.GetSystemMetrics(win32con.SM_XVIRTUALSCREEN)
|
||||||
|
top = win32api.GetSystemMetrics(win32con.SM_YVIRTUALSCREEN)
|
||||||
|
width = win32api.GetSystemMetrics(win32con.SM_CXVIRTUALSCREEN)
|
||||||
|
height = win32api.GetSystemMetrics(win32con.SM_CYVIRTUALSCREEN)
|
||||||
|
|
||||||
|
right = left + width
|
||||||
|
bottom = top + height
|
||||||
|
|
||||||
|
rect = (left, top, right, bottom)
|
||||||
|
try:
|
||||||
|
checkRect(rect)
|
||||||
|
except ValueError:
|
||||||
|
lastRect = None
|
||||||
|
else:
|
||||||
|
if rect == lastRect:
|
||||||
|
return rect
|
||||||
|
else:
|
||||||
|
lastRect = rect
|
||||||
|
|
||||||
|
raise RectFailed("Could not get stable rect information after %d tries; "
|
||||||
|
"last was %r." % (tries, lastRect))
|
||||||
|
|
||||||
|
|
||||||
|
def getDisplayRects():
|
||||||
|
"""
|
||||||
|
Returns a list containing tuples with the coordinates of each display that is
|
||||||
|
making up the virtual screen. This list is ordered by display number.
|
||||||
|
|
||||||
|
Each tuple in the list is (left, top, right, bottom), specifically (
|
||||||
|
the x-coordinate of the upper-left corner of the display,
|
||||||
|
the y-coordinate of the upper-left corner of the display,
|
||||||
|
the x-coordinate of the lower-right corner of the display,
|
||||||
|
the y-coordinate of the lower-right corner of the display
|
||||||
|
)
|
||||||
|
|
||||||
|
Note that the (0, 0) origin is the top-left corner of the main display (not
|
||||||
|
necessarily Display 1). If you have parts of any monitor to the left or
|
||||||
|
above the top-left corner of the main display, you will see some negative x/y
|
||||||
|
coordinates.
|
||||||
|
|
||||||
|
Internally, this grabs the geometry from Windows at least twice to avoid
|
||||||
|
getting bad geometry during changes to the display configuration.
|
||||||
|
|
||||||
|
Raises L{RectFailed} if the geometry cannot be retrieved, though
|
||||||
|
this failure mode has never been observed.
|
||||||
|
"""
|
||||||
|
HANDLE_MONITOR, HDC_MONITOR, SCREEN_RECT = range(3)
|
||||||
|
|
||||||
|
# My experiments show this needs to be no more than 3 (for 4 iterations
|
||||||
|
# through the loop), but use 150 in case there are pathological systems.
|
||||||
|
# Note that one iteration of the loop takes about 90us on a Q6600.
|
||||||
|
tries = 150
|
||||||
|
lastRects = None
|
||||||
|
for _ in range(tries):
|
||||||
|
try:
|
||||||
|
monitors = win32api.EnumDisplayMonitors(None, None)
|
||||||
|
except SystemError:
|
||||||
|
# If you are changing your monitor configuration while EnumDisplayMonitors
|
||||||
|
# is enumerating the displays, it may throw SystemError. We just try
|
||||||
|
# again in this case.
|
||||||
|
lastRects = None
|
||||||
|
else:
|
||||||
|
for m in monitors:
|
||||||
|
m[HDC_MONITOR].Close()
|
||||||
|
rects = list(m[SCREEN_RECT] for m in monitors)
|
||||||
|
try:
|
||||||
|
for rect in rects:
|
||||||
|
checkRect(rect)
|
||||||
|
except ValueError:
|
||||||
|
lastRects = None
|
||||||
|
else:
|
||||||
|
if rects == lastRects:
|
||||||
|
return rects
|
||||||
|
else:
|
||||||
|
lastRects = rects
|
||||||
|
|
||||||
|
raise RectFailed("Could not get stable rect information after %d tries; "
|
||||||
|
"last was %r." % (tries, lastRects))
|
||||||
|
|
||||||
|
|
||||||
|
class GrabFailed(Exception):
|
||||||
|
"""
|
||||||
|
Could not take a screenshot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def deleteDCAndBitMap(dc, bitmap):
|
||||||
|
dc.DeleteDC()
|
||||||
|
handle = bitmap.GetHandle()
|
||||||
|
# Trying to DeleteObject(0) will throw an exception; it can be 0 in the case
|
||||||
|
# of an untouched win32ui.CreateBitmap()
|
||||||
|
if handle != 0:
|
||||||
|
win32gui.DeleteObject(handle)
|
||||||
|
|
||||||
|
# In case someone rightfully imported the private helper before we made it public
|
||||||
|
_deleteDCAndBitMap = deleteDCAndBitMap
|
||||||
|
|
||||||
|
|
||||||
|
def getDCAndBitMap(saveBmpFilename=None, rect=None):
|
||||||
|
"""
|
||||||
|
Returns a (DC, PyCBitmap). On the returned DC ("device context"), you
|
||||||
|
*must* call aDC.DeleteDC(). On the returned PyCBitmap, you *must* call
|
||||||
|
win32gui.DeleteObject(aPyCBitmap.GetHandle()).
|
||||||
|
|
||||||
|
If C{saveBmpFilename} is provided, a .bmp will be saved to the specified
|
||||||
|
filename. This does not require PIL. The .bmp file will have the same
|
||||||
|
bit-depth as the screen; it is not guaranteed to be 32-bit. If you provide
|
||||||
|
this argument, you still must clean up the returned objects, as mentioned.
|
||||||
|
|
||||||
|
If C{rect} is not C{None}, instead of capturing the entire virtual screen,
|
||||||
|
only the region inside the rect will be captured. C{rect} is a tuple of (
|
||||||
|
the x-coordinate of the upper-left corner of the virtual screen,
|
||||||
|
the y-coordinate of the upper-left corner of the virtual screen,
|
||||||
|
the x-coordinate of the lower-right corner of the virtual screen,
|
||||||
|
the y-coordinate of the lower-right corner of the virtual screen
|
||||||
|
)
|
||||||
|
|
||||||
|
Note that both x and y coordinates may be negative; the (0, 0) origin is
|
||||||
|
determined by the top-left corner of the main display (not necessarily
|
||||||
|
Display 1).
|
||||||
|
|
||||||
|
Raises L{GrabFailed} if unable to take a screenshot (e.g. due to locked
|
||||||
|
workstation, no display, or active UAC elevation screen).
|
||||||
|
|
||||||
|
Raises L{ValueError} if C{rect}'s computed width or height is zero or
|
||||||
|
negative, or if rect contains nonsense.
|
||||||
|
"""
|
||||||
|
if rect is None:
|
||||||
|
try:
|
||||||
|
rect = getVirtualScreenRect()
|
||||||
|
except RectFailed as e:
|
||||||
|
raise GrabFailed("Error during getVirtualScreenRect: " + str(e))
|
||||||
|
# rect is already checked
|
||||||
|
else:
|
||||||
|
checkRect(rect)
|
||||||
|
|
||||||
|
left, top, right, bottom = rect
|
||||||
|
width = right - left
|
||||||
|
height = bottom - top
|
||||||
|
|
||||||
|
hwndDesktop = win32gui.GetDesktopWindow()
|
||||||
|
|
||||||
|
# Retrieve the device context (DC) for the entire virtual screen.
|
||||||
|
hwndDevice = win32gui.GetWindowDC(hwndDesktop)
|
||||||
|
##print("device", hwndDevice)
|
||||||
|
assert isinstance(hwndDevice, (int, long)), hwndDevice
|
||||||
|
|
||||||
|
mfcDC = win32ui.CreateDCFromHandle(hwndDevice)
|
||||||
|
try:
|
||||||
|
saveDC = mfcDC.CreateCompatibleDC()
|
||||||
|
saveBitMap = win32ui.CreateBitmap()
|
||||||
|
# Above line is assumed to never raise an exception.
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
saveBitMap.CreateCompatibleBitmap(mfcDC, width, height)
|
||||||
|
except (win32ui.error, OverflowError) as e:
|
||||||
|
raise GrabFailed("Could not CreateCompatibleBitmap("
|
||||||
|
"mfcDC, %r, %r) - perhaps too big? Error was: %s" % (width, height, e))
|
||||||
|
saveDC.SelectObject(saveBitMap)
|
||||||
|
try:
|
||||||
|
saveDC.BitBlt((0, 0), (width, height), mfcDC, (left, top), win32con.SRCCOPY)
|
||||||
|
except win32ui.error as e:
|
||||||
|
raise GrabFailed("Error during BitBlt. "
|
||||||
|
"Possible reasons: locked workstation, no display, "
|
||||||
|
"or an active UAC elevation screen. Error was: " + str(e))
|
||||||
|
if saveBmpFilename is not None:
|
||||||
|
saveBitMap.SaveBitmapFile(saveDC, saveBmpFilename)
|
||||||
|
except:
|
||||||
|
deleteDCAndBitMap(saveDC, saveBitMap)
|
||||||
|
# Let's just hope the above line doesn't raise an exception
|
||||||
|
# (or it will mask the previous exception)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
mfcDC.DeleteDC()
|
||||||
|
|
||||||
|
return saveDC, saveBitMap
|
||||||
|
|
||||||
|
|
||||||
|
class BITMAPINFOHEADER(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('biSize', ctypes.c_uint32),
|
||||||
|
('biWidth', ctypes.c_int),
|
||||||
|
('biHeight', ctypes.c_int),
|
||||||
|
('biPlanes', ctypes.c_short),
|
||||||
|
('biBitCount', ctypes.c_short),
|
||||||
|
('biCompression', ctypes.c_uint32),
|
||||||
|
('biSizeImage', ctypes.c_uint32),
|
||||||
|
('biXPelsPerMeter', ctypes.c_long),
|
||||||
|
('biYPelsPerMeter', ctypes.c_long),
|
||||||
|
('biClrUsed', ctypes.c_uint32),
|
||||||
|
('biClrImportant', ctypes.c_uint32)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class BITMAPINFO(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('bmiHeader', BITMAPINFOHEADER),
|
||||||
|
('bmiColors', ctypes.c_ulong * 3)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class DIBFailed(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def getBGR32(dc, bitmap):
|
||||||
|
"""
|
||||||
|
Returns a (raw BGR str, (width, height)) for C{dc}, C{bitmap}.
|
||||||
|
Guaranteed to be 32-bit. Note that the origin of the returned image is
|
||||||
|
in the bottom-left corner, and the image has 32-bit line padding.
|
||||||
|
"""
|
||||||
|
bmpInfo = bitmap.GetInfo()
|
||||||
|
width, height = bmpInfo['bmWidth'], bmpInfo['bmHeight']
|
||||||
|
|
||||||
|
bmi = BITMAPINFO()
|
||||||
|
ctypes.memset(ctypes.byref(bmi), 0x00, ctypes.sizeof(bmi))
|
||||||
|
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
|
||||||
|
bmi.bmiHeader.biWidth = width
|
||||||
|
bmi.bmiHeader.biHeight = height
|
||||||
|
bmi.bmiHeader.biBitCount = 24
|
||||||
|
bmi.bmiHeader.biPlanes = 1
|
||||||
|
|
||||||
|
bufferLen = height * ((width * 3 + 3) & -4)
|
||||||
|
pbBits = ctypes.create_string_buffer(bufferLen)
|
||||||
|
|
||||||
|
ret = ctypes.windll.gdi32.GetDIBits(
|
||||||
|
dc.GetHandleAttrib(),
|
||||||
|
bitmap.GetHandle(),
|
||||||
|
0,
|
||||||
|
height,
|
||||||
|
ctypes.byref(pbBits),
|
||||||
|
ctypes.pointer(bmi),
|
||||||
|
win32con.DIB_RGB_COLORS)
|
||||||
|
if ret == 0:
|
||||||
|
raise DIBFailed("Return code 0 from GetDIBits")
|
||||||
|
|
||||||
|
assert len(pbBits.raw) == bufferLen, len(pbBits.raw)
|
||||||
|
|
||||||
|
return pbBits.raw, (width, height)
|
||||||
|
|
||||||
|
|
||||||
|
def _getRectAsImage(rect):
|
||||||
|
try:
|
||||||
|
# Pillow or PIL
|
||||||
|
from PIL import Image
|
||||||
|
except ImportError:
|
||||||
|
# some old PIL installations
|
||||||
|
import Image
|
||||||
|
|
||||||
|
dc, bitmap = getDCAndBitMap(rect=rect)
|
||||||
|
try:
|
||||||
|
bmpInfo = bitmap.GetInfo()
|
||||||
|
# bmpInfo is something like {
|
||||||
|
# 'bmType': 0, 'bmWidthBytes': 5120, 'bmHeight': 1024,
|
||||||
|
# 'bmBitsPixel': 32, 'bmPlanes': 1, 'bmWidth': 1280}
|
||||||
|
##print(bmpInfo)
|
||||||
|
size = (bmpInfo['bmWidth'], bmpInfo['bmHeight'])
|
||||||
|
|
||||||
|
if bmpInfo['bmBitsPixel'] == 32:
|
||||||
|
# Use GetBitmapBits and BGRX if the bpp == 32, because
|
||||||
|
# it's ~15% faster than the method below.
|
||||||
|
data = bitmap.GetBitmapBits(True) # asString=True
|
||||||
|
return Image.frombuffer(
|
||||||
|
'RGB', size, data, 'raw', 'BGRX', 0, 1)
|
||||||
|
else:
|
||||||
|
# If bpp != 32, we cannot use GetBitmapBits, because it
|
||||||
|
# does not return a 24/32-bit image when the screen is at
|
||||||
|
# a lower color depth.
|
||||||
|
try:
|
||||||
|
data, size = getBGR32(dc, bitmap)
|
||||||
|
except DIBFailed as e:
|
||||||
|
raise GrabFailed("getBGR32 failed. Error was " + str(e))
|
||||||
|
# BGR, 32-bit line padding, origo in lower left corner
|
||||||
|
return Image.frombuffer(
|
||||||
|
'RGB', size, data, 'raw', 'BGR', (size[0] * 3 + 3) & -4, -1)
|
||||||
|
finally:
|
||||||
|
deleteDCAndBitMap(dc, bitmap)
|
||||||
|
|
||||||
|
|
||||||
|
def getScreenAsImage():
|
||||||
|
"""
|
||||||
|
Returns a PIL Image object (mode RGB) of the entire virtual screen.
|
||||||
|
|
||||||
|
Raises L{GrabFailed} if unable to take a screenshot (e.g. due to locked
|
||||||
|
workstation, no display, or active UAC elevation screen).
|
||||||
|
"""
|
||||||
|
return _getRectAsImage(None)
|
||||||
|
|
||||||
|
|
||||||
|
def normalizeRects(rects):
|
||||||
|
"""
|
||||||
|
Normalize a list of rects (e.g. as returned by L{getDisplayRects()})
|
||||||
|
to make all coordinates >= 0. This is useful if you want to do your own
|
||||||
|
cropping of an entire virtual screen as returned by L{getScreenAsImage()}.
|
||||||
|
"""
|
||||||
|
smallestX = min(rect[0] for rect in rects)
|
||||||
|
smallestY = min(rect[1] for rect in rects)
|
||||||
|
return list(
|
||||||
|
(-smallestX + left,
|
||||||
|
-smallestY + top,
|
||||||
|
-smallestX + right,
|
||||||
|
-smallestY + bottom) for left, top, right, bottom in rects
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def getDisplaysAsImages():
|
||||||
|
"""
|
||||||
|
Returns a list of PIL Image objects (mode RGB), one for each display.
|
||||||
|
This list is ordered by display number.
|
||||||
|
|
||||||
|
Internally, this captures the entire virtual screen and then crops out each
|
||||||
|
Image based on display information. This method ensures that all displays
|
||||||
|
are captured at the same time (or as close to it as Windows permits).
|
||||||
|
|
||||||
|
Raises L{GrabFailed} if unable to take a screenshot (e.g. due to locked
|
||||||
|
workstation, no display, or active UAC elevation screen).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
rects = getDisplayRects()
|
||||||
|
except RectFailed as e:
|
||||||
|
raise GrabFailed("Error during getDisplayRects: " + str(e))
|
||||||
|
# im has an origin at (0, 0) in the top-left corner of the virtual screen,
|
||||||
|
# but our `rect`s have a (0, 0) origin in the top-left corner of the main
|
||||||
|
# display. So we normalize all coordinates in the rects to be >= 0.
|
||||||
|
normalizedRects = normalizeRects(rects)
|
||||||
|
im = getScreenAsImage()
|
||||||
|
|
||||||
|
return list(im.crop(rect) for rect in normalizedRects)
|
||||||
|
|
||||||
|
|
||||||
|
def getRectAsImage(rect):
|
||||||
|
"""
|
||||||
|
Returns a PIL Image object (mode RGB) of the region inside the rect.
|
||||||
|
|
||||||
|
See the L{getDCAndBitMap} docstring for C{rect} documentation.
|
||||||
|
|
||||||
|
Raises L{GrabFailed} if unable to take a screenshot (e.g. due to locked
|
||||||
|
workstation, no display, or active UAC elevation screen).
|
||||||
|
|
||||||
|
Raises L{ValueError} if C{rect}'s computed width or height is zero or
|
||||||
|
negative, or if rect contains nonsense.
|
||||||
|
|
||||||
|
Raises L{TypeError} if C{rect} is C{None}.
|
||||||
|
"""
|
||||||
|
if rect is None:
|
||||||
|
raise TypeError("Expected a tuple for rect, got None")
|
||||||
|
return _getRectAsImage(rect)
|
||||||
|
|
||||||
|
|
||||||
|
def saveScreenToBmp(bmpFilename):
|
||||||
|
"""
|
||||||
|
Save a screenshot of the entire virtual screen to a .bmp file. Does not
|
||||||
|
require PIL. The .bmp file will have the same bit-depth as the screen;
|
||||||
|
it is not guaranteed to be 32-bit.
|
||||||
|
|
||||||
|
Raises L{GrabFailed} if unable to take a screenshot (e.g. due to locked
|
||||||
|
workstation, no display, or active UAC elevation screen).
|
||||||
|
"""
|
||||||
|
dc, bitmap = getDCAndBitMap(bmpFilename)
|
||||||
|
deleteDCAndBitMap(dc, bitmap)
|
||||||
|
|
||||||
|
|
||||||
|
def saveRectToBmp(bmpFilename, rect):
|
||||||
|
"""
|
||||||
|
Save a screenshot of the region inside the rect to a .bmp file. Does not
|
||||||
|
require PIL. The .bmp file will have the same bit-depth as the screen;
|
||||||
|
it is not guaranteed to be 32-bit.
|
||||||
|
|
||||||
|
See the L{getDCAndBitMap} docstring for C{rect} documentation.
|
||||||
|
|
||||||
|
Raises L{GrabFailed} if unable to take a screenshot (e.g. due to locked
|
||||||
|
workstation, no display, or active UAC elevation screen).
|
||||||
|
|
||||||
|
Raises L{ValueError} if C{rect}'s computed width or height is zero or
|
||||||
|
negative, or if rect contains nonsense.
|
||||||
|
|
||||||
|
Raises L{TypeError} if C{rect} is C{None}.
|
||||||
|
"""
|
||||||
|
if rect is None:
|
||||||
|
raise TypeError("Expected a tuple for rect, got None")
|
||||||
|
dc, bitmap = getDCAndBitMap(bmpFilename, rect)
|
||||||
|
deleteDCAndBitMap(dc, bitmap)
|
||||||
|
|
||||||
|
|
||||||
|
def _demo():
|
||||||
|
# Save the entire virtual screen as a BMP (no PIL required)
|
||||||
|
saveScreenToBmp('screencapture_entire.bmp')
|
||||||
|
|
||||||
|
# Save an arbitrary rectangle of the virtual screen as a BMP (no PIL required)
|
||||||
|
saveRectToBmp('screencapture_256_256.bmp', rect=(0, 0, 256, 256))
|
||||||
|
|
||||||
|
# Save the entire virtual screen as a PNG
|
||||||
|
entireScreen = getScreenAsImage()
|
||||||
|
entireScreen.save('screencapture_entire.png', format='png')
|
||||||
|
|
||||||
|
# Get bounding rectangles for all displays, in display order
|
||||||
|
print("Display rects are:", getDisplayRects())
|
||||||
|
# -> something like [(0, 0, 1280, 1024), (-1280, 0, 0, 1024), (1280, -176, 3200, 1024)]
|
||||||
|
|
||||||
|
# Capture an arbitrary rectangle of the virtual screen: (left, top, right, bottom)
|
||||||
|
rect256 = getRectAsImage((0, 0, 256, 256))
|
||||||
|
rect256.save('screencapture_256_256.png', format='png')
|
||||||
|
|
||||||
|
# Unsynchronized capture, one display at a time.
|
||||||
|
# If you need all displays, use getDisplaysAsImages() instead.
|
||||||
|
for displayNumber, rect in enumerate(getDisplayRects(), 1):
|
||||||
|
imDisplay = getRectAsImage(rect)
|
||||||
|
imDisplay.save('screencapture_unsync_display_%d.png' % (displayNumber,), format='png')
|
||||||
|
|
||||||
|
# Synchronized capture, entire virtual screen at once, cropped to one Image per display.
|
||||||
|
for displayNumber, im in enumerate(getDisplaysAsImages(), 1):
|
||||||
|
im.save('screencapture_sync_display_%d.png' % (displayNumber,), format='png')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
_demo()
|
@ -0,0 +1,49 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from desktopmagic.screengrab_win32 import GrabFailed, getScreenAsImage, getDisplaysAsImages, getRectAsImage
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("""\
|
||||||
|
This program helps you test whether screengrab_win32 has memory leaks
|
||||||
|
and other problems. It takes a screenshot repeatedly and discards it.
|
||||||
|
|
||||||
|
Open Task Manager and make sure Physical Memory % is not ballooning.
|
||||||
|
Memory leaks might not be blamed on the python process itself (which
|
||||||
|
will show low memory usage).
|
||||||
|
|
||||||
|
Lock the workstation for a few minutes; make sure there are no leaks
|
||||||
|
and that there are no uncaught exceptions here.
|
||||||
|
|
||||||
|
Repeat above after RDPing into the workstation and minimizing RDP;
|
||||||
|
this is like disconnecting the monitor.
|
||||||
|
|
||||||
|
Change your color depth settings. Add and remove monitors. RDP
|
||||||
|
in at 256 colors.
|
||||||
|
""")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
getScreenAsImage()
|
||||||
|
print("S", end=" ")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except GrabFailed as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
getDisplaysAsImages()
|
||||||
|
print("D", end=" ")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except GrabFailed as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
getRectAsImage((0, 0, 1, 1))
|
||||||
|
print("R", end=" ")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except GrabFailed as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,33 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
from desktopmagic.screengrab_win32 import getDisplayRects
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("""\
|
||||||
|
This program constantly polls your display rect information and prints
|
||||||
|
it when it changes.
|
||||||
|
|
||||||
|
This can be used to make sure getDisplayRects is free from desync bugs
|
||||||
|
that occur during monitor configuration changes.
|
||||||
|
""")
|
||||||
|
lastRects = None
|
||||||
|
count = 0
|
||||||
|
start = time.time()
|
||||||
|
while True:
|
||||||
|
if count != 0 and count % 1000 == 0:
|
||||||
|
end = time.time()
|
||||||
|
##print(end - start, "for 1000 calls")
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
rects = getDisplayRects()
|
||||||
|
if rects != lastRects:
|
||||||
|
print(rects)
|
||||||
|
lastRects = rects
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,33 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
from desktopmagic.screengrab_win32 import getVirtualScreenRect
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("""\
|
||||||
|
This program constantly polls your virtual screen rect information and
|
||||||
|
prints it when it changes.
|
||||||
|
|
||||||
|
This can be used to make sure getVirtualScreenRect is free from desync
|
||||||
|
bugs that occur during monitor configuration changes.
|
||||||
|
""")
|
||||||
|
lastRect = None
|
||||||
|
count = 0
|
||||||
|
start = time.time()
|
||||||
|
while True:
|
||||||
|
if count != 0 and count % 1000 == 0:
|
||||||
|
end = time.time()
|
||||||
|
##print(end - start, "for 1000 calls")
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
rect = getVirtualScreenRect()
|
||||||
|
if rect != lastRect:
|
||||||
|
print(rect)
|
||||||
|
lastRect = rect
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,118 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import os
|
||||||
|
try:
|
||||||
|
# Needed for Python 2.6 compatibility
|
||||||
|
import unittest2 as unittest
|
||||||
|
except ImportError:
|
||||||
|
import unittest
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from desktopmagic.screengrab_win32 import getDisplayRects, saveRectToBmp, getRectAsImage, GrabFailed
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Pillow or PIL
|
||||||
|
from PIL import Image
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
# some old PIL installations
|
||||||
|
import Image
|
||||||
|
except ImportError:
|
||||||
|
Image = None
|
||||||
|
|
||||||
|
|
||||||
|
class GetDisplayRectsTest(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Tests for L{getDisplayRects}
|
||||||
|
"""
|
||||||
|
def test_getDisplayRectsReturnsList(self):
|
||||||
|
"""
|
||||||
|
L{getDisplayRects} returns a list of length >= 1 with a tuple containing 4 integers,
|
||||||
|
representing the geometry of each display.
|
||||||
|
"""
|
||||||
|
regions = getDisplayRects()
|
||||||
|
##print("Display rects are:", regions)
|
||||||
|
self.assertIsInstance(regions, list)
|
||||||
|
for region in regions:
|
||||||
|
self.assertIsInstance(region, tuple)
|
||||||
|
for num in region:
|
||||||
|
self.assertIsInstance(num, int)
|
||||||
|
|
||||||
|
|
||||||
|
def disabled_test_getDisplayRectsDoesNotLeak(self):
|
||||||
|
"""
|
||||||
|
Calling L{getDisplayRects} 100,000 times does not leak memory (you'll have to
|
||||||
|
open taskmgr to make sure.)
|
||||||
|
|
||||||
|
Disabled because Ivan manually confirmed that it does not leak.
|
||||||
|
"""
|
||||||
|
print("Open taskmgr.exe to make sure I'm not leaking memory right now.")
|
||||||
|
for i in xrange(100000):
|
||||||
|
getDisplayRects()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RectTests(unittest.TestCase):
|
||||||
|
def _tryUnlink(self, fname):
|
||||||
|
try:
|
||||||
|
os.unlink(fname)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_workingCase(self):
|
||||||
|
if not Image:
|
||||||
|
self.skipTest("No PIL or Pillow")
|
||||||
|
|
||||||
|
fname = tempfile.mktemp()
|
||||||
|
self.addCleanup(self._tryUnlink, fname)
|
||||||
|
saveRectToBmp(fname, rect=(0, 0, 200, 100))
|
||||||
|
with open(fname, "rb") as f:
|
||||||
|
im = Image.open(f)
|
||||||
|
self.assertEqual((200, 100), im.size)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalidRect(self):
|
||||||
|
fname = tempfile.mktemp()
|
||||||
|
self.addCleanup(self._tryUnlink, fname)
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, 100, 100)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, 99, 100)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, 100, 99)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, 100, None)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, "100", None)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100.0, 100, 101, 101)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, 101, 101.0)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, 200, 200, 200)))
|
||||||
|
self.assertRaises(TypeError, lambda: saveRectToBmp(fname, rect=None))
|
||||||
|
self.assertRaises(TypeError, lambda: getRectAsImage(rect=None))
|
||||||
|
|
||||||
|
|
||||||
|
def test_1x1SizeRect(self):
|
||||||
|
if not Image:
|
||||||
|
self.skipTest("No PIL or Pillow")
|
||||||
|
|
||||||
|
fname = tempfile.mktemp() + '.bmp'
|
||||||
|
fnamePng = tempfile.mktemp() + '.png'
|
||||||
|
self.addCleanup(self._tryUnlink, fname)
|
||||||
|
self.addCleanup(self._tryUnlink, fnamePng)
|
||||||
|
saveRectToBmp(fname, rect=(100, 100, 101, 101))
|
||||||
|
|
||||||
|
with open(fname, "rb") as f:
|
||||||
|
im = Image.open(f)
|
||||||
|
self.assertEqual((1, 1), im.size)
|
||||||
|
|
||||||
|
im = getRectAsImage(rect=(100, 100, 101, 101))
|
||||||
|
self.assertEqual((1, 1), im.size)
|
||||||
|
im.save(fnamePng, format='png')
|
||||||
|
|
||||||
|
with open(fnamePng, "rb") as f:
|
||||||
|
im = Image.open(f)
|
||||||
|
self.assertEqual((1, 1), im.size)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rectTooBig(self):
|
||||||
|
fname = tempfile.mktemp()
|
||||||
|
self.addCleanup(self._tryUnlink, fname)
|
||||||
|
# Note that 26000x26000 is big enough to fail it on my system
|
||||||
|
self.assertRaises(GrabFailed, lambda: saveRectToBmp(fname, rect=(0, 0, 2600000, 2600000)))
|
||||||
|
self.assertRaises(GrabFailed, lambda: saveRectToBmp(fname, rect=(0, 0, 2600000, 260000000000000000)))
|
@ -0,0 +1,5 @@
|
|||||||
|
#!C:\Abs\Archive\scopeSrcUL\OpenRPA\Resources\WPy32-3720\python-3.7.2\python.exe
|
||||||
|
|
||||||
|
from desktopmagic.scripts.screengrab_torture_test import main
|
||||||
|
|
||||||
|
main()
|
@ -0,0 +1 @@
|
|||||||
|
pip
|
@ -0,0 +1,21 @@
|
|||||||
|
Desktopmagic license:
|
||||||
|
|
||||||
|
Copyright (c) 2011 Ivan Kozik
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
@ -0,0 +1,21 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: Desktopmagic
|
||||||
|
Version: 14.3.11
|
||||||
|
Summary: Robust multi-monitor screenshot grabber (Windows-only right now)
|
||||||
|
Home-page: https://github.com/ludios/Desktopmagic
|
||||||
|
Author: Ivan Kozik
|
||||||
|
Author-email: ivan@ludios.org
|
||||||
|
License: UNKNOWN
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Classifier: Programming Language :: Python :: 2
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Environment :: Win32 (MS Windows)
|
||||||
|
Classifier: Operating System :: Microsoft :: Windows
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
|
Classifier: Topic :: Multimedia :: Graphics :: Capture :: Screen Capture
|
||||||
|
|
||||||
|
UNKNOWN
|
||||||
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
|||||||
|
../../Scripts/screengrab_torture_test,sha256=M0lDv6izfH8QGmYx6xFoeZVTv_Jx9nLVHh0Ci0YkjPc,80
|
||||||
|
Desktopmagic-14.3.11.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
Desktopmagic-14.3.11.dist-info/LICENSE.txt,sha256=81jpSYGoAoxvlRjLO2NTfL0AHHyDJAMMHSCRURRATKo,1077
|
||||||
|
Desktopmagic-14.3.11.dist-info/METADATA,sha256=nt1JjqpQb_fvEJac3LbQY1AONRJEVtX3gs6zM2dT4XI,696
|
||||||
|
Desktopmagic-14.3.11.dist-info/RECORD,,
|
||||||
|
Desktopmagic-14.3.11.dist-info/WHEEL,sha256=JtBte-IW7C3UcYx3ZpZORq-KtnjVj4xdM4AJCTZPivc,98
|
||||||
|
Desktopmagic-14.3.11.dist-info/top_level.txt,sha256=Y3lWkebSjPOAXP7mzfuDAlkutR0_QwhHEENcXjARMOI,13
|
||||||
|
desktopmagic/__init__.py,sha256=DO7UEI9j-9pPANCC0gBBxPxTTRTd11-vYhlURkvGqFM,24
|
||||||
|
desktopmagic/__pycache__/__init__.cpython-37.pyc,,
|
||||||
|
desktopmagic/__pycache__/screengrab_win32.cpython-37.pyc,,
|
||||||
|
desktopmagic/screengrab_win32.py,sha256=-lu2xdSdAWnWFDJMjxfpIcRiqc78TRGC1-XrFFhery4,15899
|
||||||
|
desktopmagic/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
desktopmagic/scripts/__pycache__/__init__.cpython-37.pyc,,
|
||||||
|
desktopmagic/scripts/__pycache__/screengrab_torture_test.cpython-37.pyc,,
|
||||||
|
desktopmagic/scripts/__pycache__/screengrab_watch_display_rect.cpython-37.pyc,,
|
||||||
|
desktopmagic/scripts/__pycache__/screengrab_watch_virtual_screen_rect.cpython-37.pyc,,
|
||||||
|
desktopmagic/scripts/screengrab_torture_test.py,sha256=-MpUliKrDSZdx_0gutvK7dslrRT7orbvhfbjy-LqIug,1209
|
||||||
|
desktopmagic/scripts/screengrab_watch_display_rect.py,sha256=AhFxIHaCUnL755ds4RoaxhNknbfdzWnLz_3n-fmjhF4,682
|
||||||
|
desktopmagic/scripts/screengrab_watch_virtual_screen_rect.py,sha256=yDhmeajPPnV8a2WVvJoVnv6l8DUQpAMubOZfUPwQCaU,697
|
||||||
|
desktopmagic/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
desktopmagic/test/__pycache__/__init__.cpython-37.pyc,,
|
||||||
|
desktopmagic/test/__pycache__/test_screengrab_win32.cpython-37.pyc,,
|
||||||
|
desktopmagic/test/test_screengrab_win32.py,sha256=vWdtKxFD1CvudkuTeSgoiZwiqTNfgcMW9eq7KjMddjg,3652
|
@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.33.1)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: cp37-none-any
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
desktopmagic
|
@ -0,0 +1 @@
|
|||||||
|
__version__ = '14.3.11'
|
@ -0,0 +1,494 @@
|
|||||||
|
"""
|
||||||
|
Robust functions for grabbing and saving screenshots on Windows.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import win32gui
|
||||||
|
import win32ui
|
||||||
|
import win32con
|
||||||
|
import win32api
|
||||||
|
|
||||||
|
try:
|
||||||
|
long
|
||||||
|
except NameError:
|
||||||
|
# Python 3
|
||||||
|
long = int
|
||||||
|
|
||||||
|
|
||||||
|
def checkRect(rect):
|
||||||
|
"""
|
||||||
|
Check C{rect} for validity.
|
||||||
|
|
||||||
|
Raises L{ValueError} if C{rect}'s computed width or height is zero or
|
||||||
|
negative, or if rect contains nonsense.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
left, top, right, bottom = rect
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("%r is not a valid rect; must contain 4 ints" % (rect,))
|
||||||
|
if not all(isinstance(x, (int, long)) for x in rect):
|
||||||
|
raise ValueError("%r is not a valid rect; must contain 4 ints" % (rect,))
|
||||||
|
width = right - left
|
||||||
|
height = bottom - top
|
||||||
|
if width <= 0 or height <= 0:
|
||||||
|
raise ValueError("%r is not a valid rect; width and height must not be "
|
||||||
|
"zero or negative" % (rect,))
|
||||||
|
|
||||||
|
|
||||||
|
class RectFailed(Exception):
|
||||||
|
"""
|
||||||
|
Could not get information about the virtual screen or a display.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def getVirtualScreenRect():
|
||||||
|
"""
|
||||||
|
Returns a tuple containing (
|
||||||
|
the x-coordinate of the upper-left corner of the virtual screen,
|
||||||
|
the y-coordinate of the upper-left corner of the virtual screen,
|
||||||
|
the x-coordinate of the lower-right corner of the virtual screen,
|
||||||
|
the y-coordinate of the lower-right corner of the virtual screen
|
||||||
|
)
|
||||||
|
|
||||||
|
Note that both x and y coordinates may be negative; the (0, 0) origin is
|
||||||
|
determined by the top-left corner of the main display (not necessarily
|
||||||
|
Display 1).
|
||||||
|
|
||||||
|
Internally, this grabs the geometry from Windows at least twice to avoid
|
||||||
|
getting bad geometry during changes to the display configuration.
|
||||||
|
|
||||||
|
Raises L{RectFailed} if the geometry cannot be retrieved, though
|
||||||
|
this failure mode has never been observed.
|
||||||
|
"""
|
||||||
|
# Note that one iteration of the loop takes about 2us on a Q6600.
|
||||||
|
tries = 150
|
||||||
|
lastRect = None
|
||||||
|
for _ in range(tries):
|
||||||
|
# Get dimensions of the entire virtual screen. Note that left/top may be negative.
|
||||||
|
# Any of these may return nonsense numbers during display configuration
|
||||||
|
# changes (not just "desync" between our calls, but numbers that make little
|
||||||
|
# sense, as if some Windows state doesn't change synchronously.)
|
||||||
|
# This is why we get them at least twice.
|
||||||
|
left = win32api.GetSystemMetrics(win32con.SM_XVIRTUALSCREEN)
|
||||||
|
top = win32api.GetSystemMetrics(win32con.SM_YVIRTUALSCREEN)
|
||||||
|
width = win32api.GetSystemMetrics(win32con.SM_CXVIRTUALSCREEN)
|
||||||
|
height = win32api.GetSystemMetrics(win32con.SM_CYVIRTUALSCREEN)
|
||||||
|
|
||||||
|
right = left + width
|
||||||
|
bottom = top + height
|
||||||
|
|
||||||
|
rect = (left, top, right, bottom)
|
||||||
|
try:
|
||||||
|
checkRect(rect)
|
||||||
|
except ValueError:
|
||||||
|
lastRect = None
|
||||||
|
else:
|
||||||
|
if rect == lastRect:
|
||||||
|
return rect
|
||||||
|
else:
|
||||||
|
lastRect = rect
|
||||||
|
|
||||||
|
raise RectFailed("Could not get stable rect information after %d tries; "
|
||||||
|
"last was %r." % (tries, lastRect))
|
||||||
|
|
||||||
|
|
||||||
|
def getDisplayRects():
|
||||||
|
"""
|
||||||
|
Returns a list containing tuples with the coordinates of each display that is
|
||||||
|
making up the virtual screen. This list is ordered by display number.
|
||||||
|
|
||||||
|
Each tuple in the list is (left, top, right, bottom), specifically (
|
||||||
|
the x-coordinate of the upper-left corner of the display,
|
||||||
|
the y-coordinate of the upper-left corner of the display,
|
||||||
|
the x-coordinate of the lower-right corner of the display,
|
||||||
|
the y-coordinate of the lower-right corner of the display
|
||||||
|
)
|
||||||
|
|
||||||
|
Note that the (0, 0) origin is the top-left corner of the main display (not
|
||||||
|
necessarily Display 1). If you have parts of any monitor to the left or
|
||||||
|
above the top-left corner of the main display, you will see some negative x/y
|
||||||
|
coordinates.
|
||||||
|
|
||||||
|
Internally, this grabs the geometry from Windows at least twice to avoid
|
||||||
|
getting bad geometry during changes to the display configuration.
|
||||||
|
|
||||||
|
Raises L{RectFailed} if the geometry cannot be retrieved, though
|
||||||
|
this failure mode has never been observed.
|
||||||
|
"""
|
||||||
|
HANDLE_MONITOR, HDC_MONITOR, SCREEN_RECT = range(3)
|
||||||
|
|
||||||
|
# My experiments show this needs to be no more than 3 (for 4 iterations
|
||||||
|
# through the loop), but use 150 in case there are pathological systems.
|
||||||
|
# Note that one iteration of the loop takes about 90us on a Q6600.
|
||||||
|
tries = 150
|
||||||
|
lastRects = None
|
||||||
|
for _ in range(tries):
|
||||||
|
try:
|
||||||
|
monitors = win32api.EnumDisplayMonitors(None, None)
|
||||||
|
except SystemError:
|
||||||
|
# If you are changing your monitor configuration while EnumDisplayMonitors
|
||||||
|
# is enumerating the displays, it may throw SystemError. We just try
|
||||||
|
# again in this case.
|
||||||
|
lastRects = None
|
||||||
|
else:
|
||||||
|
for m in monitors:
|
||||||
|
m[HDC_MONITOR].Close()
|
||||||
|
rects = list(m[SCREEN_RECT] for m in monitors)
|
||||||
|
try:
|
||||||
|
for rect in rects:
|
||||||
|
checkRect(rect)
|
||||||
|
except ValueError:
|
||||||
|
lastRects = None
|
||||||
|
else:
|
||||||
|
if rects == lastRects:
|
||||||
|
return rects
|
||||||
|
else:
|
||||||
|
lastRects = rects
|
||||||
|
|
||||||
|
raise RectFailed("Could not get stable rect information after %d tries; "
|
||||||
|
"last was %r." % (tries, lastRects))
|
||||||
|
|
||||||
|
|
||||||
|
class GrabFailed(Exception):
|
||||||
|
"""
|
||||||
|
Could not take a screenshot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def deleteDCAndBitMap(dc, bitmap):
|
||||||
|
dc.DeleteDC()
|
||||||
|
handle = bitmap.GetHandle()
|
||||||
|
# Trying to DeleteObject(0) will throw an exception; it can be 0 in the case
|
||||||
|
# of an untouched win32ui.CreateBitmap()
|
||||||
|
if handle != 0:
|
||||||
|
win32gui.DeleteObject(handle)
|
||||||
|
|
||||||
|
# In case someone rightfully imported the private helper before we made it public
|
||||||
|
_deleteDCAndBitMap = deleteDCAndBitMap
|
||||||
|
|
||||||
|
|
||||||
|
def getDCAndBitMap(saveBmpFilename=None, rect=None):
|
||||||
|
"""
|
||||||
|
Returns a (DC, PyCBitmap). On the returned DC ("device context"), you
|
||||||
|
*must* call aDC.DeleteDC(). On the returned PyCBitmap, you *must* call
|
||||||
|
win32gui.DeleteObject(aPyCBitmap.GetHandle()).
|
||||||
|
|
||||||
|
If C{saveBmpFilename} is provided, a .bmp will be saved to the specified
|
||||||
|
filename. This does not require PIL. The .bmp file will have the same
|
||||||
|
bit-depth as the screen; it is not guaranteed to be 32-bit. If you provide
|
||||||
|
this argument, you still must clean up the returned objects, as mentioned.
|
||||||
|
|
||||||
|
If C{rect} is not C{None}, instead of capturing the entire virtual screen,
|
||||||
|
only the region inside the rect will be captured. C{rect} is a tuple of (
|
||||||
|
the x-coordinate of the upper-left corner of the virtual screen,
|
||||||
|
the y-coordinate of the upper-left corner of the virtual screen,
|
||||||
|
the x-coordinate of the lower-right corner of the virtual screen,
|
||||||
|
the y-coordinate of the lower-right corner of the virtual screen
|
||||||
|
)
|
||||||
|
|
||||||
|
Note that both x and y coordinates may be negative; the (0, 0) origin is
|
||||||
|
determined by the top-left corner of the main display (not necessarily
|
||||||
|
Display 1).
|
||||||
|
|
||||||
|
Raises L{GrabFailed} if unable to take a screenshot (e.g. due to locked
|
||||||
|
workstation, no display, or active UAC elevation screen).
|
||||||
|
|
||||||
|
Raises L{ValueError} if C{rect}'s computed width or height is zero or
|
||||||
|
negative, or if rect contains nonsense.
|
||||||
|
"""
|
||||||
|
if rect is None:
|
||||||
|
try:
|
||||||
|
rect = getVirtualScreenRect()
|
||||||
|
except RectFailed as e:
|
||||||
|
raise GrabFailed("Error during getVirtualScreenRect: " + str(e))
|
||||||
|
# rect is already checked
|
||||||
|
else:
|
||||||
|
checkRect(rect)
|
||||||
|
|
||||||
|
left, top, right, bottom = rect
|
||||||
|
width = right - left
|
||||||
|
height = bottom - top
|
||||||
|
|
||||||
|
hwndDesktop = win32gui.GetDesktopWindow()
|
||||||
|
|
||||||
|
# Retrieve the device context (DC) for the entire virtual screen.
|
||||||
|
hwndDevice = win32gui.GetWindowDC(hwndDesktop)
|
||||||
|
##print("device", hwndDevice)
|
||||||
|
assert isinstance(hwndDevice, (int, long)), hwndDevice
|
||||||
|
|
||||||
|
mfcDC = win32ui.CreateDCFromHandle(hwndDevice)
|
||||||
|
try:
|
||||||
|
saveDC = mfcDC.CreateCompatibleDC()
|
||||||
|
saveBitMap = win32ui.CreateBitmap()
|
||||||
|
# Above line is assumed to never raise an exception.
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
saveBitMap.CreateCompatibleBitmap(mfcDC, width, height)
|
||||||
|
except (win32ui.error, OverflowError) as e:
|
||||||
|
raise GrabFailed("Could not CreateCompatibleBitmap("
|
||||||
|
"mfcDC, %r, %r) - perhaps too big? Error was: %s" % (width, height, e))
|
||||||
|
saveDC.SelectObject(saveBitMap)
|
||||||
|
try:
|
||||||
|
saveDC.BitBlt((0, 0), (width, height), mfcDC, (left, top), win32con.SRCCOPY)
|
||||||
|
except win32ui.error as e:
|
||||||
|
raise GrabFailed("Error during BitBlt. "
|
||||||
|
"Possible reasons: locked workstation, no display, "
|
||||||
|
"or an active UAC elevation screen. Error was: " + str(e))
|
||||||
|
if saveBmpFilename is not None:
|
||||||
|
saveBitMap.SaveBitmapFile(saveDC, saveBmpFilename)
|
||||||
|
except:
|
||||||
|
deleteDCAndBitMap(saveDC, saveBitMap)
|
||||||
|
# Let's just hope the above line doesn't raise an exception
|
||||||
|
# (or it will mask the previous exception)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
mfcDC.DeleteDC()
|
||||||
|
|
||||||
|
return saveDC, saveBitMap
|
||||||
|
|
||||||
|
|
||||||
|
class BITMAPINFOHEADER(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('biSize', ctypes.c_uint32),
|
||||||
|
('biWidth', ctypes.c_int),
|
||||||
|
('biHeight', ctypes.c_int),
|
||||||
|
('biPlanes', ctypes.c_short),
|
||||||
|
('biBitCount', ctypes.c_short),
|
||||||
|
('biCompression', ctypes.c_uint32),
|
||||||
|
('biSizeImage', ctypes.c_uint32),
|
||||||
|
('biXPelsPerMeter', ctypes.c_long),
|
||||||
|
('biYPelsPerMeter', ctypes.c_long),
|
||||||
|
('biClrUsed', ctypes.c_uint32),
|
||||||
|
('biClrImportant', ctypes.c_uint32)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class BITMAPINFO(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('bmiHeader', BITMAPINFOHEADER),
|
||||||
|
('bmiColors', ctypes.c_ulong * 3)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class DIBFailed(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def getBGR32(dc, bitmap):
|
||||||
|
"""
|
||||||
|
Returns a (raw BGR str, (width, height)) for C{dc}, C{bitmap}.
|
||||||
|
Guaranteed to be 32-bit. Note that the origin of the returned image is
|
||||||
|
in the bottom-left corner, and the image has 32-bit line padding.
|
||||||
|
"""
|
||||||
|
bmpInfo = bitmap.GetInfo()
|
||||||
|
width, height = bmpInfo['bmWidth'], bmpInfo['bmHeight']
|
||||||
|
|
||||||
|
bmi = BITMAPINFO()
|
||||||
|
ctypes.memset(ctypes.byref(bmi), 0x00, ctypes.sizeof(bmi))
|
||||||
|
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
|
||||||
|
bmi.bmiHeader.biWidth = width
|
||||||
|
bmi.bmiHeader.biHeight = height
|
||||||
|
bmi.bmiHeader.biBitCount = 24
|
||||||
|
bmi.bmiHeader.biPlanes = 1
|
||||||
|
|
||||||
|
bufferLen = height * ((width * 3 + 3) & -4)
|
||||||
|
pbBits = ctypes.create_string_buffer(bufferLen)
|
||||||
|
|
||||||
|
ret = ctypes.windll.gdi32.GetDIBits(
|
||||||
|
dc.GetHandleAttrib(),
|
||||||
|
bitmap.GetHandle(),
|
||||||
|
0,
|
||||||
|
height,
|
||||||
|
ctypes.byref(pbBits),
|
||||||
|
ctypes.pointer(bmi),
|
||||||
|
win32con.DIB_RGB_COLORS)
|
||||||
|
if ret == 0:
|
||||||
|
raise DIBFailed("Return code 0 from GetDIBits")
|
||||||
|
|
||||||
|
assert len(pbBits.raw) == bufferLen, len(pbBits.raw)
|
||||||
|
|
||||||
|
return pbBits.raw, (width, height)
|
||||||
|
|
||||||
|
|
||||||
|
def _getRectAsImage(rect):
|
||||||
|
try:
|
||||||
|
# Pillow or PIL
|
||||||
|
from PIL import Image
|
||||||
|
except ImportError:
|
||||||
|
# some old PIL installations
|
||||||
|
import Image
|
||||||
|
|
||||||
|
dc, bitmap = getDCAndBitMap(rect=rect)
|
||||||
|
try:
|
||||||
|
bmpInfo = bitmap.GetInfo()
|
||||||
|
# bmpInfo is something like {
|
||||||
|
# 'bmType': 0, 'bmWidthBytes': 5120, 'bmHeight': 1024,
|
||||||
|
# 'bmBitsPixel': 32, 'bmPlanes': 1, 'bmWidth': 1280}
|
||||||
|
##print(bmpInfo)
|
||||||
|
size = (bmpInfo['bmWidth'], bmpInfo['bmHeight'])
|
||||||
|
|
||||||
|
if bmpInfo['bmBitsPixel'] == 32:
|
||||||
|
# Use GetBitmapBits and BGRX if the bpp == 32, because
|
||||||
|
# it's ~15% faster than the method below.
|
||||||
|
data = bitmap.GetBitmapBits(True) # asString=True
|
||||||
|
return Image.frombuffer(
|
||||||
|
'RGB', size, data, 'raw', 'BGRX', 0, 1)
|
||||||
|
else:
|
||||||
|
# If bpp != 32, we cannot use GetBitmapBits, because it
|
||||||
|
# does not return a 24/32-bit image when the screen is at
|
||||||
|
# a lower color depth.
|
||||||
|
try:
|
||||||
|
data, size = getBGR32(dc, bitmap)
|
||||||
|
except DIBFailed as e:
|
||||||
|
raise GrabFailed("getBGR32 failed. Error was " + str(e))
|
||||||
|
# BGR, 32-bit line padding, origo in lower left corner
|
||||||
|
return Image.frombuffer(
|
||||||
|
'RGB', size, data, 'raw', 'BGR', (size[0] * 3 + 3) & -4, -1)
|
||||||
|
finally:
|
||||||
|
deleteDCAndBitMap(dc, bitmap)
|
||||||
|
|
||||||
|
|
||||||
|
def getScreenAsImage():
|
||||||
|
"""
|
||||||
|
Returns a PIL Image object (mode RGB) of the entire virtual screen.
|
||||||
|
|
||||||
|
Raises L{GrabFailed} if unable to take a screenshot (e.g. due to locked
|
||||||
|
workstation, no display, or active UAC elevation screen).
|
||||||
|
"""
|
||||||
|
return _getRectAsImage(None)
|
||||||
|
|
||||||
|
|
||||||
|
def normalizeRects(rects):
|
||||||
|
"""
|
||||||
|
Normalize a list of rects (e.g. as returned by L{getDisplayRects()})
|
||||||
|
to make all coordinates >= 0. This is useful if you want to do your own
|
||||||
|
cropping of an entire virtual screen as returned by L{getScreenAsImage()}.
|
||||||
|
"""
|
||||||
|
smallestX = min(rect[0] for rect in rects)
|
||||||
|
smallestY = min(rect[1] for rect in rects)
|
||||||
|
return list(
|
||||||
|
(-smallestX + left,
|
||||||
|
-smallestY + top,
|
||||||
|
-smallestX + right,
|
||||||
|
-smallestY + bottom) for left, top, right, bottom in rects
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def getDisplaysAsImages():
|
||||||
|
"""
|
||||||
|
Returns a list of PIL Image objects (mode RGB), one for each display.
|
||||||
|
This list is ordered by display number.
|
||||||
|
|
||||||
|
Internally, this captures the entire virtual screen and then crops out each
|
||||||
|
Image based on display information. This method ensures that all displays
|
||||||
|
are captured at the same time (or as close to it as Windows permits).
|
||||||
|
|
||||||
|
Raises L{GrabFailed} if unable to take a screenshot (e.g. due to locked
|
||||||
|
workstation, no display, or active UAC elevation screen).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
rects = getDisplayRects()
|
||||||
|
except RectFailed as e:
|
||||||
|
raise GrabFailed("Error during getDisplayRects: " + str(e))
|
||||||
|
# im has an origin at (0, 0) in the top-left corner of the virtual screen,
|
||||||
|
# but our `rect`s have a (0, 0) origin in the top-left corner of the main
|
||||||
|
# display. So we normalize all coordinates in the rects to be >= 0.
|
||||||
|
normalizedRects = normalizeRects(rects)
|
||||||
|
im = getScreenAsImage()
|
||||||
|
|
||||||
|
return list(im.crop(rect) for rect in normalizedRects)
|
||||||
|
|
||||||
|
|
||||||
|
def getRectAsImage(rect):
|
||||||
|
"""
|
||||||
|
Returns a PIL Image object (mode RGB) of the region inside the rect.
|
||||||
|
|
||||||
|
See the L{getDCAndBitMap} docstring for C{rect} documentation.
|
||||||
|
|
||||||
|
Raises L{GrabFailed} if unable to take a screenshot (e.g. due to locked
|
||||||
|
workstation, no display, or active UAC elevation screen).
|
||||||
|
|
||||||
|
Raises L{ValueError} if C{rect}'s computed width or height is zero or
|
||||||
|
negative, or if rect contains nonsense.
|
||||||
|
|
||||||
|
Raises L{TypeError} if C{rect} is C{None}.
|
||||||
|
"""
|
||||||
|
if rect is None:
|
||||||
|
raise TypeError("Expected a tuple for rect, got None")
|
||||||
|
return _getRectAsImage(rect)
|
||||||
|
|
||||||
|
|
||||||
|
def saveScreenToBmp(bmpFilename):
|
||||||
|
"""
|
||||||
|
Save a screenshot of the entire virtual screen to a .bmp file. Does not
|
||||||
|
require PIL. The .bmp file will have the same bit-depth as the screen;
|
||||||
|
it is not guaranteed to be 32-bit.
|
||||||
|
|
||||||
|
Raises L{GrabFailed} if unable to take a screenshot (e.g. due to locked
|
||||||
|
workstation, no display, or active UAC elevation screen).
|
||||||
|
"""
|
||||||
|
dc, bitmap = getDCAndBitMap(bmpFilename)
|
||||||
|
deleteDCAndBitMap(dc, bitmap)
|
||||||
|
|
||||||
|
|
||||||
|
def saveRectToBmp(bmpFilename, rect):
|
||||||
|
"""
|
||||||
|
Save a screenshot of the region inside the rect to a .bmp file. Does not
|
||||||
|
require PIL. The .bmp file will have the same bit-depth as the screen;
|
||||||
|
it is not guaranteed to be 32-bit.
|
||||||
|
|
||||||
|
See the L{getDCAndBitMap} docstring for C{rect} documentation.
|
||||||
|
|
||||||
|
Raises L{GrabFailed} if unable to take a screenshot (e.g. due to locked
|
||||||
|
workstation, no display, or active UAC elevation screen).
|
||||||
|
|
||||||
|
Raises L{ValueError} if C{rect}'s computed width or height is zero or
|
||||||
|
negative, or if rect contains nonsense.
|
||||||
|
|
||||||
|
Raises L{TypeError} if C{rect} is C{None}.
|
||||||
|
"""
|
||||||
|
if rect is None:
|
||||||
|
raise TypeError("Expected a tuple for rect, got None")
|
||||||
|
dc, bitmap = getDCAndBitMap(bmpFilename, rect)
|
||||||
|
deleteDCAndBitMap(dc, bitmap)
|
||||||
|
|
||||||
|
|
||||||
|
def _demo():
|
||||||
|
# Save the entire virtual screen as a BMP (no PIL required)
|
||||||
|
saveScreenToBmp('screencapture_entire.bmp')
|
||||||
|
|
||||||
|
# Save an arbitrary rectangle of the virtual screen as a BMP (no PIL required)
|
||||||
|
saveRectToBmp('screencapture_256_256.bmp', rect=(0, 0, 256, 256))
|
||||||
|
|
||||||
|
# Save the entire virtual screen as a PNG
|
||||||
|
entireScreen = getScreenAsImage()
|
||||||
|
entireScreen.save('screencapture_entire.png', format='png')
|
||||||
|
|
||||||
|
# Get bounding rectangles for all displays, in display order
|
||||||
|
print("Display rects are:", getDisplayRects())
|
||||||
|
# -> something like [(0, 0, 1280, 1024), (-1280, 0, 0, 1024), (1280, -176, 3200, 1024)]
|
||||||
|
|
||||||
|
# Capture an arbitrary rectangle of the virtual screen: (left, top, right, bottom)
|
||||||
|
rect256 = getRectAsImage((0, 0, 256, 256))
|
||||||
|
rect256.save('screencapture_256_256.png', format='png')
|
||||||
|
|
||||||
|
# Unsynchronized capture, one display at a time.
|
||||||
|
# If you need all displays, use getDisplaysAsImages() instead.
|
||||||
|
for displayNumber, rect in enumerate(getDisplayRects(), 1):
|
||||||
|
imDisplay = getRectAsImage(rect)
|
||||||
|
imDisplay.save('screencapture_unsync_display_%d.png' % (displayNumber,), format='png')
|
||||||
|
|
||||||
|
# Synchronized capture, entire virtual screen at once, cropped to one Image per display.
|
||||||
|
for displayNumber, im in enumerate(getDisplaysAsImages(), 1):
|
||||||
|
im.save('screencapture_sync_display_%d.png' % (displayNumber,), format='png')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
_demo()
|
@ -0,0 +1,49 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from desktopmagic.screengrab_win32 import GrabFailed, getScreenAsImage, getDisplaysAsImages, getRectAsImage
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("""\
|
||||||
|
This program helps you test whether screengrab_win32 has memory leaks
|
||||||
|
and other problems. It takes a screenshot repeatedly and discards it.
|
||||||
|
|
||||||
|
Open Task Manager and make sure Physical Memory % is not ballooning.
|
||||||
|
Memory leaks might not be blamed on the python process itself (which
|
||||||
|
will show low memory usage).
|
||||||
|
|
||||||
|
Lock the workstation for a few minutes; make sure there are no leaks
|
||||||
|
and that there are no uncaught exceptions here.
|
||||||
|
|
||||||
|
Repeat above after RDPing into the workstation and minimizing RDP;
|
||||||
|
this is like disconnecting the monitor.
|
||||||
|
|
||||||
|
Change your color depth settings. Add and remove monitors. RDP
|
||||||
|
in at 256 colors.
|
||||||
|
""")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
getScreenAsImage()
|
||||||
|
print("S", end=" ")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except GrabFailed as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
getDisplaysAsImages()
|
||||||
|
print("D", end=" ")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except GrabFailed as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
getRectAsImage((0, 0, 1, 1))
|
||||||
|
print("R", end=" ")
|
||||||
|
sys.stdout.flush()
|
||||||
|
except GrabFailed as e:
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,33 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
from desktopmagic.screengrab_win32 import getDisplayRects
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("""\
|
||||||
|
This program constantly polls your display rect information and prints
|
||||||
|
it when it changes.
|
||||||
|
|
||||||
|
This can be used to make sure getDisplayRects is free from desync bugs
|
||||||
|
that occur during monitor configuration changes.
|
||||||
|
""")
|
||||||
|
lastRects = None
|
||||||
|
count = 0
|
||||||
|
start = time.time()
|
||||||
|
while True:
|
||||||
|
if count != 0 and count % 1000 == 0:
|
||||||
|
end = time.time()
|
||||||
|
##print(end - start, "for 1000 calls")
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
rects = getDisplayRects()
|
||||||
|
if rects != lastRects:
|
||||||
|
print(rects)
|
||||||
|
lastRects = rects
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,33 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
from desktopmagic.screengrab_win32 import getVirtualScreenRect
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("""\
|
||||||
|
This program constantly polls your virtual screen rect information and
|
||||||
|
prints it when it changes.
|
||||||
|
|
||||||
|
This can be used to make sure getVirtualScreenRect is free from desync
|
||||||
|
bugs that occur during monitor configuration changes.
|
||||||
|
""")
|
||||||
|
lastRect = None
|
||||||
|
count = 0
|
||||||
|
start = time.time()
|
||||||
|
while True:
|
||||||
|
if count != 0 and count % 1000 == 0:
|
||||||
|
end = time.time()
|
||||||
|
##print(end - start, "for 1000 calls")
|
||||||
|
start = time.time()
|
||||||
|
|
||||||
|
rect = getVirtualScreenRect()
|
||||||
|
if rect != lastRect:
|
||||||
|
print(rect)
|
||||||
|
lastRect = rect
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,118 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import os
|
||||||
|
try:
|
||||||
|
# Needed for Python 2.6 compatibility
|
||||||
|
import unittest2 as unittest
|
||||||
|
except ImportError:
|
||||||
|
import unittest
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from desktopmagic.screengrab_win32 import getDisplayRects, saveRectToBmp, getRectAsImage, GrabFailed
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Pillow or PIL
|
||||||
|
from PIL import Image
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
# some old PIL installations
|
||||||
|
import Image
|
||||||
|
except ImportError:
|
||||||
|
Image = None
|
||||||
|
|
||||||
|
|
||||||
|
class GetDisplayRectsTest(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Tests for L{getDisplayRects}
|
||||||
|
"""
|
||||||
|
def test_getDisplayRectsReturnsList(self):
|
||||||
|
"""
|
||||||
|
L{getDisplayRects} returns a list of length >= 1 with a tuple containing 4 integers,
|
||||||
|
representing the geometry of each display.
|
||||||
|
"""
|
||||||
|
regions = getDisplayRects()
|
||||||
|
##print("Display rects are:", regions)
|
||||||
|
self.assertIsInstance(regions, list)
|
||||||
|
for region in regions:
|
||||||
|
self.assertIsInstance(region, tuple)
|
||||||
|
for num in region:
|
||||||
|
self.assertIsInstance(num, int)
|
||||||
|
|
||||||
|
|
||||||
|
def disabled_test_getDisplayRectsDoesNotLeak(self):
|
||||||
|
"""
|
||||||
|
Calling L{getDisplayRects} 100,000 times does not leak memory (you'll have to
|
||||||
|
open taskmgr to make sure.)
|
||||||
|
|
||||||
|
Disabled because Ivan manually confirmed that it does not leak.
|
||||||
|
"""
|
||||||
|
print("Open taskmgr.exe to make sure I'm not leaking memory right now.")
|
||||||
|
for i in xrange(100000):
|
||||||
|
getDisplayRects()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RectTests(unittest.TestCase):
|
||||||
|
def _tryUnlink(self, fname):
|
||||||
|
try:
|
||||||
|
os.unlink(fname)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_workingCase(self):
|
||||||
|
if not Image:
|
||||||
|
self.skipTest("No PIL or Pillow")
|
||||||
|
|
||||||
|
fname = tempfile.mktemp()
|
||||||
|
self.addCleanup(self._tryUnlink, fname)
|
||||||
|
saveRectToBmp(fname, rect=(0, 0, 200, 100))
|
||||||
|
with open(fname, "rb") as f:
|
||||||
|
im = Image.open(f)
|
||||||
|
self.assertEqual((200, 100), im.size)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalidRect(self):
|
||||||
|
fname = tempfile.mktemp()
|
||||||
|
self.addCleanup(self._tryUnlink, fname)
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, 100, 100)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, 99, 100)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, 100, 99)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, 100, None)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, "100", None)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100.0, 100, 101, 101)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, 101, 101.0)))
|
||||||
|
self.assertRaises(ValueError, lambda: saveRectToBmp(fname, rect=(100, 100, 200, 200, 200)))
|
||||||
|
self.assertRaises(TypeError, lambda: saveRectToBmp(fname, rect=None))
|
||||||
|
self.assertRaises(TypeError, lambda: getRectAsImage(rect=None))
|
||||||
|
|
||||||
|
|
||||||
|
def test_1x1SizeRect(self):
|
||||||
|
if not Image:
|
||||||
|
self.skipTest("No PIL or Pillow")
|
||||||
|
|
||||||
|
fname = tempfile.mktemp() + '.bmp'
|
||||||
|
fnamePng = tempfile.mktemp() + '.png'
|
||||||
|
self.addCleanup(self._tryUnlink, fname)
|
||||||
|
self.addCleanup(self._tryUnlink, fnamePng)
|
||||||
|
saveRectToBmp(fname, rect=(100, 100, 101, 101))
|
||||||
|
|
||||||
|
with open(fname, "rb") as f:
|
||||||
|
im = Image.open(f)
|
||||||
|
self.assertEqual((1, 1), im.size)
|
||||||
|
|
||||||
|
im = getRectAsImage(rect=(100, 100, 101, 101))
|
||||||
|
self.assertEqual((1, 1), im.size)
|
||||||
|
im.save(fnamePng, format='png')
|
||||||
|
|
||||||
|
with open(fnamePng, "rb") as f:
|
||||||
|
im = Image.open(f)
|
||||||
|
self.assertEqual((1, 1), im.size)
|
||||||
|
|
||||||
|
|
||||||
|
def test_rectTooBig(self):
|
||||||
|
fname = tempfile.mktemp()
|
||||||
|
self.addCleanup(self._tryUnlink, fname)
|
||||||
|
# Note that 26000x26000 is big enough to fail it on my system
|
||||||
|
self.assertRaises(GrabFailed, lambda: saveRectToBmp(fname, rect=(0, 0, 2600000, 2600000)))
|
||||||
|
self.assertRaises(GrabFailed, lambda: saveRectToBmp(fname, rect=(0, 0, 2600000, 260000000000000000)))
|
@ -0,0 +1,5 @@
|
|||||||
|
#!C:\Abs\Archive\scopeSrcUL\OpenRPA\Resources\WPy64-3720\python-3.7.2.amd64\python.exe
|
||||||
|
|
||||||
|
from desktopmagic.scripts.screengrab_torture_test import main
|
||||||
|
|
||||||
|
main()
|
Loading…
Reference in new issue