-
Notifications
You must be signed in to change notification settings - Fork 10
/
desktop_magic.py
288 lines (233 loc) · 8.95 KB
/
desktop_magic.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
"""
Robust functions for grabbing and saving screenshots on Windows.
"""
# TODO: support capture of individual displays (and at the same time with a "single screenshot")
# Use GetDeviceCaps; see http://msdn.microsoft.com/en-us/library/dd144877%28v=vs.85%29.aspx
import ctypes
import win32gui
import win32ui
import win32con
import win32api
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 GrabFailed(Exception):
"""
Could not take a screenshot.
"""
class MonitorSelectionOutOfBounds(Exception):
'''
Argument out of bounds
'''
class BoundingBoxOutOfRange(Exception):
'''
Coordinates are too large for the current resolution
'''
class DIBFailed(Exception):
pass
def _deleteDCAndBitMap(dc, bitmap):
dc.DeleteDC()
win32gui.DeleteObject(bitmap.GetHandle())
def getMonitorCoordinates(targetMonitor):
'''
Enumerates the available monitor. Return the
Screen Dimensions of the selected monitor.
'''
HMONITOR = 0
HDCMONITOR = 1
SCREENRECT = 2
try:
monitors = win32api.EnumDisplayMonitors(None, None)
if targetMonitor > len(monitors)-1:
raise MonitorSelectionOutOfBounds("Monitor argument exceeds attached number of devices.\n"
"There are only %d display devices attached.\n" % len(monitors) +
"Please select appropriate device ( 0=Primary, 1=Secondary, etc..)." )
left,top,right,bottom = monitors[targetMonitor][SCREENRECT]
width = right - left
height = bottom
finally:
# I can't figure out what to do with the handle to the Monitor
# that gets returned from EnumDisplayMonitors (the first object in
# the tuple). Trying to close it throws an error.. Does it not need
# cleaned up at all? Most of the winApi is back magic to me...
# These device context handles were the only things that I could Close()
for monitor in monitors:
monitor[HDCMONITOR].Close()
return (left, top, width, height)
def getSecondaryMonitorCoordinates():
'''
Enumerates the available monitors. Return the
Screen Dimensions of the secondary monitor.
'''
HMONITOR = 0
HDCMONITOR = 1
SCREENRECT = 2
try:
monitors = win32api.EnumDisplayMonitors(None, None)
# if targetMonitor > len(monitors)-1:
# raise MonitorSelectionOutOfBounds("Monitor argument exceeds attached number of devices.\n"
# "There are only %d display devices attached.\n" % len(monitors) +
# "Please select appropriate device ( 0=Primary, 1=Secondary, etc..)." )
left,top,right,bottom = monitors[-1][SCREENRECT]
width = right - left
height = bottom
finally:
# I can't figure out what to do with the handle to the Monitor
# that gets returned from EnumDisplayMonitors (the first object in
# the tuple). Trying to close it throws an error.. Does it not need
# cleaned up at all? Most of the winApi is back magic to me...
# These device context handles were the only things that I could Close()
for monitor in monitors:
monitor[HDCMONITOR].Close()
return (left, top, width, height)
def getDCAndBitMap(saveBmpFilename=None, bbox=None):
"""
Returns a (DC, PyCBitmap). On the returned PyCBitmap, you *must* call
win32gui.DeleteObject(aPyCBitmap.GetHandle()). On the returned DC,
you *must* call aDC.DeleteDC()
"""
hwnd = win32gui.GetDesktopWindow()
if bbox:
left, top, width, height = bbox
if (left < win32api.GetSystemMetrics(win32con.SM_XVIRTUALSCREEN) or
top < win32api.GetSystemMetrics(win32con.SM_YVIRTUALSCREEN) or
width > win32api.GetSystemMetrics(win32con.SM_CXVIRTUALSCREEN) or
height > win32api.GetSystemMetrics(win32con.SM_CYVIRTUALSCREEN)):
raise Exception('Invalid bounding box. Range exceeds available screen area.')
width = width - left
height = height - top
else:
# Get complete virtual screen, including all monitors.
left = win32api.GetSystemMetrics(win32con.SM_XVIRTUALSCREEN)
top = win32api.GetSystemMetrics(win32con.SM_YVIRTUALSCREEN)
width = win32api.GetSystemMetrics(win32con.SM_CXVIRTUALSCREEN)
height = win32api.GetSystemMetrics(win32con.SM_CYVIRTUALSCREEN)
##print "L", left, "T", top, "dim:", width, "x", height
# Retrieve the device context (DC) for the entire window.
hwndDevice = win32gui.GetWindowDC(hwnd)
##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:
saveBitMap.CreateCompatibleBitmap(mfcDC, width, height)
saveDC.SelectObject(saveBitMap)
try:
saveDC.BitBlt((0, 0), (width, height), mfcDC, (left, top), win32con.SRCCOPY)
except win32ui.error, 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
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.GetDIBgetits(
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 getScreenAsImage(bbox=None):
"""
Returns a PIL Image object (mode RGB) of the current screen (incl.
all monitors).
bbox = boundingBox. Used to snap a subarea of the screen.
A tuple of (x, y, width, height).
"""
import Image
dc, bitmap = getDCAndBitMap(bbox=bbox)
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, 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 saveScreenToBmp(bmpFilename, bbox=None):
"""
Save a screenshot (incl. all monitors) 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.
bbox = boundingBox. Used to snap a subarea of the screen.
A tuple of (x, y, width, height).
"""
dc, bitmap = getDCAndBitMap(saveBmpFilename=bmpFilename, bbox=bbox)
_deleteDCAndBitMap(dc, bitmap)
def _demo():
saveNames = ['allMonitors', 'primaryMonitor', 'secondaryMonitor', 'boundingTestOne', 'boundingTestTwo']
params = [None, getMonitorCoordinates(0), getMonitorCoordinates(1), (0,0,100,50), (400,300, 200,200)]
# for i in range(len(saveNames)):
# saveScreenToBmp( saveNames[i] + '.bmp', params[i])
# im = getScreenAsImage(params[i])
# im.save(saveNames[i] + '.png', format='png' )
if __name__ == '__main__':
im = getScreenAsImage((588, 117, 1307, 596))
im.save('testttttttt.png', 'png')