轉(zhuǎn):https://www.cnblogs.com/xieqiankun/p/usePythonForScreenShot.html
|
使用Python保存屏幕截圖(不使用PIL)
起因
在極客學(xué)院講授《使用Python編寫遠(yuǎn)程控制程序》的課程中,涉及到查看被控制電腦屏幕截圖的功能。
如果使用PIL,這個需求只需要三行代碼:
from PIL import ImageGrab
pic = ImageGrab.grab()
pic.save('1.jpg')
但是考慮到被控端應(yīng)該盡量的精簡,對其他模塊盡量少的依賴,這樣才能比較方便的部署,因此我考慮能否有一種方法,不依賴PIL來實現(xiàn)截圖的功能。
思路
由于被控端使用了win32api, 因此有一個方法:
win32api.keybd_event
這個方法可以模擬鍵盤的按鍵動作。因此,解決方法就比較的明顯了:
- 模擬鍵盤上面的“Print Screen” 鍵按下
- 從剪貼板中讀取出截圖
- 將截圖保存到本地
第一步非常的簡單,實用win32api 和 win32con,兩行代碼就能實現(xiàn):
import win32api
import win32con
win32api.keybd_event(win32con.VK_SNAPSHOT, 0)
其中win32con這個庫里面包含了很多定義好的和Windows相關(guān)的常量,而VK_SNAPSHOT就是Print Screen鍵的鍵位碼。后面的數(shù)字0表示截取整個屏幕。如果改成數(shù)字1,表示截取當(dāng)前窗口。
那么現(xiàn)在問題來了,在不實用PIL的情況下,如何將剪貼板你們的圖片保存到本地?
win32api有一個模塊 win32clipboard 是負(fù)責(zé)剪貼板相關(guān)的操作。它有一個方法:
win32clipboard.GetClipboardData(formats)
這個方法可以從剪貼板里面讀取數(shù)據(jù)。但是需要指定數(shù)據(jù)的格式。從這里可以查看到更多的標(biāo)準(zhǔn)剪貼板格式(Standard Clipboard Formats).
一開始我使用的formats是CF_BITMAP,程序返回的是一串整數(shù),懷疑應(yīng)該是一個內(nèi)存地址。這也和這個format的描述:
A handle to a bitmap (HBITMAP).
是一致的,它是一個handle。
我也嘗試過CF_TIFF, 不過程序直接報錯了,可見我使用Print Screen截圖以后,剪貼板里面的圖片格式并不是TIFF。
經(jīng)過查閱其他資料,我最后確定使用了CF_DIB。
A memory object containing a BITMAPINFO structure followed by the bitmap bits.
這個描述說明,CF_DIB返回的是一個內(nèi)存對象,包含了BIT格式圖片的信息。經(jīng)過測試使用:
win32clipboard.GetClipboardData(win32con.CF_DIB)
以后,可以得到一個很大的字符串。顯然這個字符串就是圖片的內(nèi)容了。但是當(dāng)我把這個字符串寫入到bmp格式的文件后,卻發(fā)現(xiàn)圖片無法打開。
解決辦法
在StackOverflow上,我遇到了一個非常好的老先生: Mr. martineau他為了解答了問題,并給我提供了解決辦法。以下內(nèi)容翻譯自martineau先生的回答,原文請戳->http://stackoverflow.com/a/35885108/3922976
你的方法的主要問題在于,你寫入文件的字符串缺少了.bmp 文件頭,這個文件頭是
BITMAPFILEHEADER結(jié)構(gòu)。
為了創(chuàng)建這個文件頭,使用
GetClipboardData()返回的字符串必須要進(jìn)行解碼(decoded)。對于CF_DIB格式來說,返回的字符串的前面一部分就是BOTMAPINFOHEADER。
對于各種各樣有不同種類壓縮的
DIB來說,這種文件頭結(jié)構(gòu)是非常的普遍的。不過幸好對截圖來說,只需要簡單的無壓縮的RGBA像素。
由于
BOTMAPFILEHEADER被放在了bf0ffBits的區(qū)域里,所以事情就變得很容易了。而其他的情況,例如大尺度的顏色表跟在BITMAPINFOHEADER和像素數(shù)組的開頭。
(這一段我看不太懂,還請如果有能正確解釋這段話的朋友指正。原文是:
That fact makes things much easier because otherwise determining the value to put in the bfOffBits field of the BITMAPFILEHEADER would be complicated by the fact that in most other cases there's also a variably-sized color table following the BITMAPINFOHEADER and the start of the pixel array.)
下面的代碼是一個簡單的例子(僅僅針對這個需求):
import ctypes
from ctypes.wintypes import *
import win32clipboard
from win32con import *
import sys
class BITMAPFILEHEADER(ctypes.Structure):
_pack_ = 1 # structure field byte alignment
_fields_ = [
('bfType', WORD), # file type ("BM")
('bfSize', DWORD), # file size in bytes
('bfReserved1', WORD), # must be zero
('bfReserved2', WORD), # must be zero
('bfOffBits', DWORD), # byte offset to the pixel array
]
SIZEOF_BITMAPFILEHEADER = ctypes.sizeof(BITMAPFILEHEADER)
class BITMAPINFOHEADER(ctypes.Structure):
_pack_ = 1 # structure field byte alignment
_fields_ = [
('biSize', DWORD),
('biWidth', LONG),
('biHeight', LONG),
('biPLanes', WORD),
('biBitCount', WORD),
('biCompression', DWORD),
('biSizeImage', DWORD),
('biXPelsPerMeter', LONG),
('biYPelsPerMeter', LONG),
('biClrUsed', DWORD),
('biClrImportant', DWORD)
]
SIZEOF_BITMAPINFOHEADER = ctypes.sizeof(BITMAPINFOHEADER)
win32clipboard.OpenClipboard()
try:
if win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB):
data = win32clipboard.GetClipboardData(win32clipboard.CF_DIB)
else:
print('clipboard does not contain an image in DIB format')
sys.exit(1)
finally:
win32clipboard.CloseClipboard()
bmih = BITMAPINFOHEADER()
ctypes.memmove(ctypes.pointer(bmih), data, SIZEOF_BITMAPINFOHEADER)
if bmih.biCompression != BI_BITFIELDS: # RGBA?
print('insupported compression type {}'.format(bmih.biCompression))
sys.exit(1)
bmfh = BITMAPFILEHEADER()
ctypes.memset(ctypes.pointer(bmfh), 0, SIZEOF_BITMAPFILEHEADER) # zero structure
bmfh.bfType = ord('B') | (ord('M') << 8)
bmfh.bfSize = SIZEOF_BITMAPFILEHEADER + len(data) # file size
SIZEOF_COLORTABLE = 0
bmfh.bfOffBits = SIZEOF_BITMAPFILEHEADER + SIZEOF_BITMAPINFOHEADER + SIZEOF_COLORTABLE
bmp_filename = 'clipboard.bmp'
with open(bmp_filename, 'wb') as bmp_file:
bmp_file.write(bmfh)
bmp_file.write(data)
print('file "{}" created from clipboard image'.format(bmp_filename))
經(jīng)過測試,這一段代碼成功的實現(xiàn)了讀取剪貼板的圖片并保存到本地。
分析
這段代碼使用ctypes庫來實現(xiàn)指針的功能,從而在內(nèi)存中操作數(shù)據(jù)。這里定義了兩個結(jié)構(gòu)體,BITMAPFILEHEADER 和BITMAPINFOHEADER,于是,使用sizeof獲取到了他們的大小。那么使用指針,從使用GetClipboardData()獲取到的數(shù)據(jù)的頭部開始移動,分別移動這兩個結(jié)構(gòu)體的大小,也就獲取到了這兩個結(jié)構(gòu)體在內(nèi)存中的數(shù)據(jù)。
代碼中使用了memmove和memset兩個內(nèi)存操作的方法。從ctypes的官方文檔上,我們可以看到這兩個方法有如下的定義:
ctypes.memmove(dst, src, count)
Same as the standard C memmove library function: copies count bytes from src to dst. dst and src must be integers or ctypes instances that can be converted to pointers.
ctypes.memset(dst, c, count)
Same as the standard C memset library function: fills the memory block at address dst with count bytes of value c. dst must be an integer specifying an address, or a ctypes instance.
所以可以看出,代碼里面的:
bmih = BITMAPINFOHEADER()
ctypes.memmove(ctypes.pointer(bmih), data, SIZEOF_BITMAPINFOHEADER)
從內(nèi)存中拷貝出來了BITMAPINFOHEADER這么大的一塊的數(shù)據(jù),并保存到了bmih這個變量中。
bmfh = BITMAPFILEHEADER()
ctypes.memset(ctypes.pointer(bmfh), 0, SIZEOF_BITMAPFILEHEADER)
這一段在內(nèi)存中開辟出了BITMAPFILEHEADER這么大一塊區(qū)域,并全部填充為0.
bmfh.bfType = ord('B') | (ord('M') << 8)
這一行代碼使用了位操作。首先ord('B')的值為66,換成二進(jìn)制就是1000010;ord('M')的值為77,換成二進(jìn)制就是1001101,然后向左移動8位,得到100110100000000,這個值再與1000010取位或,得到100110101000010。
最后,使用:
bmfh.bfOffBits = SIZEOF_BITMAPFILEHEADER + SIZEOF_BITMAPINFOHEADER + SIZEOF_COLORTABLE
拼裝出頭部的大小。然后以二進(jìn)制方式,首先寫文件頭, 再寫剪貼板獲取到的字符串到本地的.bmp文件中,完成圖片的生成。
總結(jié)
Python一些輪子確實非常好的提高了開發(fā)效率,例如PIL,三行代碼實現(xiàn)了我的需求。Python在快速開發(fā)方面確實非常的方便,但是涉及到底層的一些操作的時候,還是不得不使用C語言的一些接口來進(jìn)行內(nèi)存的操作。
|