本文介紹在不使用PIL的情況下,使用Python保存截屏並保存屏幕截圖到.bmp文件。通過ctypes庫使用C指針來對記憶體進行操作。
起因
在極客學院講授《使用Python編寫遠程式控制製程序》的課程中,涉及到查看被控制電腦屏幕截圖的功能。
如果使用PIL,這個需求只需要三行代碼:
from PIL import ImageGrab
pic = ImageGrab.grab()
pic.save('1.jpg')
但是考慮到被控端應該儘量的精簡,對其他模塊儘量少的依賴,這樣才能比較方便的部署,因此我考慮能否有一種方法,不依賴PIL來實現截圖的功能。
思路
由於被控端使用了win32api, 因此有一個方法:
win32api.keybd_event
這個方法可以模擬鍵盤的按鍵動作。因此,解決方法就比較的明顯了:
- 模擬鍵盤上面的“Print Screen” 鍵按下
- 從剪貼板中讀取出截圖
- 將截圖保存到本地
第一步非常的簡單,實用win32api 和 win32con,兩行代碼就能實現:
import win32api
import win32con
win32api.keybd_event(win32con.VK_SNAPSHOT, 0)
其中win32con這個庫裡面包含了很多定義好的和Windows相關的常量,而VK_SNAPSHOT就是Print Screen鍵的鍵位碼。後面的數字0表示截取整個屏幕。如果改成數字1,表示截取當前視窗。
那麼現在問題來了,在不實用PIL的情況下,如何將剪貼板你們的圖片保存到本地?
win32api有一個模塊 win32clipboard 是負責剪貼板相關的操作。它有一個方法:
win32clipboard.GetClipboardData(formats)
這個方法可以從剪貼板裡面讀取數據。但是需要指定數據的格式。從這裡可以查看到更多的標準剪貼板格式(Standard Clipboard Formats).
一開始我使用的formats是CF_BITMAP,程式返回的是一串整數,懷疑應該是一個記憶體地址。這也和這個format的描述:
A handle to a bitmap (HBITMAP).
是一致的,它是一個handle。
我也嘗試過CF_TIFF, 不過程式直接報錯了,可見我使用Print Screen截圖以後,剪貼板裡面的圖片格式並不是TIFF。
經過查閱其他資料,我最後確定使用了CF_DIB。
A memory object containing a BITMAPINFO structure followed by the bitmap bits.
這個描述說明,CF_DIB返回的是一個記憶體對象,包含了BIT格式圖片的信息。經過測試使用:
win32clipboard.GetClipboardData(win32con.CF_DIB)
以後,可以得到一個很大的字元串。顯然這個字元串就是圖片的內容了。但是當我把這個字元串寫入到bmp格式的文件後,卻發現圖片無法打開。
解決辦法
在StackOverflow上,我遇到了一個非常好的老先生: Mr. martineau他為瞭解答了問題,並給我提供瞭解決辦法。以下內容翻譯自martineau先生的回答,原文請戳->http://stackoverflow.com/a/35885108/3922976
你的方法的主要問題在於,你寫入文件的字元串缺少了.bmp 文件頭,這個文件頭是
BITMAPFILEHEADER
結構。
為了創建這個文件頭,使用
GetClipboardData()
返回的字元串必須要進行解碼(decoded)。對於CF_DIB
格式來說,返回的字元串的前面一部分就是BOTMAPINFOHEADER
。
對於各種各樣有不同種類壓縮的
DIB
來說,這種文件頭結構是非常的普遍的。不過幸好對截圖來說,只需要簡單的無壓縮的RGBA像素。
由於
BOTMAPFILEHEADER
被放在了bf0ffBits的區域里,所以事情就變得很容易了。而其他的情況,例如大尺度的顏色表跟在BITMAPINFOHEADER
和像素數組的開頭。
(這一段我看不太懂,還請如果有能正確解釋這段話的朋友指正。原文是:
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))
經過測試,這一段代碼成功的實現了讀取剪貼板的圖片並保存到本地。
分析
這段代碼使用ctypes庫來實現指針的功能,從而在記憶體中操作數據。這裡定義了兩個結構體,BITMAPFILEHEADER
和BITMAPINFOHEADER
,於是,使用sizeof獲取到了他們的大小。那麼使用指針,從使用GetClipboardData()
獲取到的數據的頭部開始移動,分別移動這兩個結構體的大小,也就獲取到了這兩個結構體在記憶體中的數據。
代碼中使用了memmove
和memset
兩個記憶體操作的方法。從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)
從記憶體中拷貝出來了BITMAPINFOHEADER
這麼大的一塊的數據,並保存到了bmih
這個變數中。
bmfh = BITMAPFILEHEADER()
ctypes.memset(ctypes.pointer(bmfh), 0, SIZEOF_BITMAPFILEHEADER)
這一段在記憶體中開闢出了BITMAPFILEHEADER
這麼大一塊區域,並全部填充為0.
bmfh.bfType = ord('B') | (ord('M') << 8)
這一行代碼使用了位操作。首先ord('B')
的值為66,換成二進位就是1000010
;ord('M')
的值為77,換成二進位就是1001101
,然後向左移動8位,得到100110100000000
,這個值再與1000010
取位或,得到100110101000010
。
最後,使用:
bmfh.bfOffBits = SIZEOF_BITMAPFILEHEADER + SIZEOF_BITMAPINFOHEADER + SIZEOF_COLORTABLE
拼裝出頭部的大小。然後以二進位方式,首先寫文件頭, 再寫剪貼板獲取到的字元串到本地的.bmp
文件中,完成圖片的生成。
總結
Python一些輪子確實非常好的提高了開發效率,例如PIL,三行代碼實現了我的需求。Python在快速開發方面確實非常的方便,但是涉及到底層的一些操作的時候,還是不得不使用C語言的一些介面來進行記憶體的操作。