使用Python保存屏幕截圖(不使用PIL)

来源:http://www.cnblogs.com/xieqiankun/archive/2016/03/11/usePythonForScreenShot.html
-Advertisement-
Play Games

本文介紹在不使用PIL的情況下,使用Python保存截屏並保存屏幕截圖到.bmp文件。通過ctypes庫使用C指針來對記憶體進行操作。


起因

極客學院講授《使用Python編寫遠程式控制製程序》的課程中,涉及到查看被控制電腦屏幕截圖的功能。

如果使用PIL,這個需求只需要三行代碼:

from PIL import ImageGrab
pic = ImageGrab.grab()
pic.save('1.jpg')

但是考慮到被控端應該儘量的精簡,對其他模塊儘量少的依賴,這樣才能比較方便的部署,因此我考慮能否有一種方法,不依賴PIL來實現截圖的功能。

思路

由於被控端使用了win32api, 因此有一個方法:

win32api.keybd_event

這個方法可以模擬鍵盤的按鍵動作。因此,解決方法就比較的明顯了:

  1. 模擬鍵盤上面的“Print Screen” 鍵按下
  2. 從剪貼板中讀取出截圖
  3. 將截圖保存到本地

第一步非常的簡單,實用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庫來實現指針的功能,從而在記憶體中操作數據。這裡定義了兩個結構體,BITMAPFILEHEADERBITMAPINFOHEADER,於是,使用sizeof獲取到了他們的大小。那麼使用指針,從使用GetClipboardData()獲取到的數據的頭部開始移動,分別移動這兩個結構體的大小,也就獲取到了這兩個結構體在記憶體中的數據。

代碼中使用了memmovememset兩個記憶體操作的方法。從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,換成二進位就是1000010ord('M')的值為77,換成二進位就是1001101,然後向左移動8位,得到100110100000000,這個值再與1000010取位或,得到100110101000010

最後,使用:

bmfh.bfOffBits = SIZEOF_BITMAPFILEHEADER + SIZEOF_BITMAPINFOHEADER + SIZEOF_COLORTABLE

拼裝出頭部的大小。然後以二進位方式,首先寫文件頭, 再寫剪貼板獲取到的字元串到本地的.bmp文件中,完成圖片的生成。

總結

Python一些輪子確實非常好的提高了開發效率,例如PIL,三行代碼實現了我的需求。Python在快速開發方面確實非常的方便,但是涉及到底層的一些操作的時候,還是不得不使用C語言的一些介面來進行記憶體的操作。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1 <Style x:Key="{x:Static GridView.GridViewScrollViewerStyleKey}" TargetType="{x:Type ScrollViewer}"> 2 <Setter Property="Focusable" Value="false"/> 3
  • 在.net開發客戶端的時候,一定會遇到用戶沒有安裝.net環境的問題,特別是現在win7,win8,win10多系統並用的時間段,很多開發者使用的是4.5的目標環境,用戶卻是使用win7的系統,這樣勢必會要安裝環境包 可是如果win7的用戶已經安裝4.5的環境呢?那麼用戶是否需要連帶著環境包一起下載
  • NuGet is a Visual Studio extension that makes it easy to install and update third-party libraries and tools in Visual Studio. NuGet is not mandatory f
  • 什麼是通用語言運行時(CLR),簡單來講: CLR是一個支持多種編程語言及多語言互操作,完整的高級虛擬機。 有點拗口,而且不是很有啟發性,但上面的文字是將又大又複雜的CLR的功能歸類以便容易理解的第一步。它從一萬英尺的高度來幫助我們理解CLR的設計目標。從這個高度明瞭CLR之後,我們可以深入其各個組
  • 葡萄城近日與微軟公司達成合作,將Wijmo 產品線的HTML5和JaveScript 控制項融合到微軟Dynamics CRMOnline 2016版中。
  • 隨手記記 先定義下標誌枚舉: 在項目的model文件夾下新建一個IsEnums.cs類 [Flags] public enum ABC {a=1,b=2,c=4, } 然後在HomeController.cs類中引用下model, 用標誌枚舉的好處就是可以進行自由組合,而標誌枚舉里定義每個都是2的N
  • 規則引擎由推理引擎發展而來,是一種嵌入在應用程式中的組件,實現了將業務決策從應用程式代碼中分離出來,並使用預定義的語義模塊編寫業務決策。接受數據輸入,解釋業務規則,並根據業務規則做出業務決策。比較常見的業務規則引擎有Drools、VisualRules 和iLog。這裡介紹另外一個C#開源工具Rul
  • 1.設置百分比顯示而且是自適應。 2. meta標簽設置 ios:正確設置 <meta name="viewport" content="width=device-width;" /> :錯誤設置 <meta name="viewport" content="width=device-width"
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...