11"""Windows GDI-based backend for MSS.
22
33Uses user32/gdi32 APIs to capture the desktop and enumerate monitors.
4+ This implementation uses CreateDIBSection for direct memory access to pixel data.
45"""
56
67from __future__ import annotations
1213 BOOL ,
1314 BYTE ,
1415 DWORD ,
16+ HANDLE ,
1517 HBITMAP ,
1618 HDC ,
1719 HGDIOBJ ,
@@ -88,7 +90,7 @@ def _errcheck(result: BOOL | _Pointer, func: Callable, arguments: tuple) -> tupl
8890 "error_msg" : winerror .strerror ,
8991 }
9092 if winerror .winerror == 0 :
91- # Some functions return NULL/0 on failure without setting last error. (Example: CreateCompatibleBitmap
93+ # Some functions return NULL/0 on failure without setting last error. (Example: CreateDIBSection
9294 # with an invalid HDC.)
9395 msg = f"Windows graphics function failed (no error provided): { func .__name__ } "
9496 raise ScreenShotError (msg , details = details )
@@ -105,12 +107,16 @@ def _errcheck(result: BOOL | _Pointer, func: Callable, arguments: tuple) -> tupl
105107CFUNCTIONS : CFunctionsErrChecked = {
106108 # Syntax: cfunction: (attr, argtypes, restype, errcheck)
107109 "BitBlt" : ("gdi32" , [HDC , INT , INT , INT , INT , HDC , INT , INT , DWORD ], BOOL , _errcheck ),
108- "CreateCompatibleBitmap" : ("gdi32" , [HDC , INT , INT ], HBITMAP , _errcheck ),
109110 "CreateCompatibleDC" : ("gdi32" , [HDC ], HDC , _errcheck ),
111+ # CreateDIBSection: ppvBits (4th param) receives a pointer to the DIB pixel data.
112+ # hSection is NULL and offset is 0 to have the system allocate the memory.
113+ "CreateDIBSection" : ("gdi32" , [HDC , POINTER (BITMAPINFO ), UINT , POINTER (LPVOID ), HANDLE , DWORD ], HBITMAP , _errcheck ),
110114 "DeleteDC" : ("gdi32" , [HDC ], HDC , _errcheck ),
111115 "DeleteObject" : ("gdi32" , [HGDIOBJ ], BOOL , _errcheck ),
112116 "EnumDisplayMonitors" : ("user32" , [HDC , LPCRECT , MONITORNUMPROC , LPARAM ], BOOL , _errcheck ),
113- "GetDIBits" : ("gdi32" , [HDC , HBITMAP , UINT , UINT , LPVOID , POINTER (BITMAPINFO ), UINT ], INT , _errcheck ),
117+ # GdiFlush flushes the calling thread's current batch of GDI operations.
118+ # This ensures DIB memory is fully updated before reading.
119+ "GdiFlush" : ("gdi32" , [], BOOL , None ),
114120 # While GetSystemMetrics will return 0 if the parameter is invalid, it will also sometimes return 0 if the
115121 # parameter is valid but the value is actually 0 (e.g., SM_CLEANBOOT on a normal boot). Thus, we do not attach an
116122 # errcheck function here.
@@ -126,6 +132,10 @@ def _errcheck(result: BOOL | _Pointer, func: Callable, arguments: tuple) -> tupl
126132class MSS (MSSBase ):
127133 """Multiple ScreenShots implementation for Microsoft Windows.
128134
135+ This implementation uses CreateDIBSection for direct memory access to pixel data,
136+ which eliminates the need for GetDIBits. The DIB pixel data is written directly
137+ to system-managed memory that we can read from.
138+
129139 This has no Windows-specific constructor parameters.
130140
131141 .. seealso::
@@ -134,7 +144,17 @@ class MSS(MSSBase):
134144 Lists constructor parameters.
135145 """
136146
137- __slots__ = {"_bmi" , "_bmp" , "_data" , "_memdc" , "_region_width_height" , "_srcdc" , "gdi32" , "user32" }
147+ __slots__ = {
148+ "_bmi" ,
149+ "_dib" ,
150+ "_dib_array" ,
151+ "_dib_bits" ,
152+ "_memdc" ,
153+ "_region_width_height" ,
154+ "_srcdc" ,
155+ "gdi32" ,
156+ "user32" ,
157+ }
138158
139159 def __init__ (self , / , ** kwargs : Any ) -> None :
140160 super ().__init__ (** kwargs )
@@ -147,29 +167,30 @@ def __init__(self, /, **kwargs: Any) -> None:
147167
148168 # Available instance-specific variables
149169 self ._region_width_height : tuple [int , int ] | None = None
150- self ._bmp : HBITMAP | None = None
170+ self ._dib : HBITMAP | None = None
171+ self ._dib_bits : LPVOID = LPVOID () # Pointer to DIB pixel data
172+ self ._dib_array : ctypes .Array [ctypes .c_char ] | None = None # Cached array view of DIB memory
151173 self ._srcdc = self .user32 .GetWindowDC (0 )
152174 self ._memdc = self .gdi32 .CreateCompatibleDC (self ._srcdc )
153- self ._data : ctypes .Array [ctypes .c_char ] | None = None
154175
155176 bmi = BITMAPINFO ()
156177 bmi .bmiHeader .biSize = ctypes .sizeof (BITMAPINFOHEADER )
157178 # biWidth and biHeight are set in _grab_impl().
158179 bmi .bmiHeader .biPlanes = 1 # Always 1
159- bmi .bmiHeader .biBitCount = 32 # See grab.__doc__ [2]
180+ bmi .bmiHeader .biBitCount = 32 # 32-bit RGBX
160181 bmi .bmiHeader .biCompression = 0 # 0 = BI_RGB (no compression)
161182 bmi .bmiHeader .biSizeImage = 0 # Windows infers the size
162183 bmi .bmiHeader .biXPelsPerMeter = 0 # Unspecified
163184 bmi .bmiHeader .biYPelsPerMeter = 0 # Unspecified
164- bmi .bmiHeader .biClrUsed = 0 # See grab.__doc__ [3]
165- bmi .bmiHeader .biClrImportant = 0 # See grab.__doc__ [3]
185+ bmi .bmiHeader .biClrUsed = 0
186+ bmi .bmiHeader .biClrImportant = 0
166187 self ._bmi = bmi
167188
168189 def _close_impl (self ) -> None :
169190 # Clean-up
170- if self ._bmp :
171- self .gdi32 .DeleteObject (self ._bmp )
172- self ._bmp = None
191+ if self ._dib :
192+ self .gdi32 .DeleteObject (self ._dib )
193+ self ._dib = None
173194
174195 if self ._memdc :
175196 self .gdi32 .DeleteDC (self ._memdc )
@@ -239,34 +260,17 @@ def callback(_monitor: HMONITOR, _data: HDC, rect: LPRECT, _dc: LPARAM) -> bool:
239260 user32 .EnumDisplayMonitors (0 , None , callback , 0 )
240261
241262 def _grab_impl (self , monitor : Monitor , / ) -> ScreenShot :
242- """Retrieve all pixels from a monitor. Pixels have to be RGB.
243-
244- In the code, there are a few interesting things:
245-
246- [1] bmi.bmiHeader.biHeight = -height
247-
248- A bottom-up DIB is specified by setting the height to a
249- positive number, while a top-down DIB is specified by
250- setting the height to a negative number.
251- https://msdn.microsoft.com/en-us/library/ms787796.aspx
252- https://msdn.microsoft.com/en-us/library/dd144879%28v=vs.85%29.aspx
253-
254-
255- [2] bmi.bmiHeader.biBitCount = 32
256- image_data = create_string_buffer(height * width * 4)
263+ """Retrieve all pixels from a monitor using CreateDIBSection.
257264
258- We grab the image in RGBX mode, so that each word is 32bit
259- and we have no striding.
260- Inspired by https://github.com/zoofIO/flexx
265+ CreateDIBSection creates a DIB with system-managed memory backing,
266+ allowing BitBlt to write directly to memory we can read. This eliminates
267+ the need for a separate GetDIBits call.
261268
262-
263- [3] bmi.bmiHeader.biClrUsed = 0
264- bmi.bmiHeader.biClrImportant = 0
265-
266- When biClrUsed and biClrImportant are set to zero, there
267- is "no" color table, so we can read the pixels of the bitmap
268- retrieved by gdi32.GetDIBits() as a sequence of RGB values.
269- Thanks to http://stackoverflow.com/a/3688682
269+ Note on biHeight: A bottom-up DIB is specified by setting the height to a
270+ positive number, while a top-down DIB is specified by setting the height
271+ to a negative number. We use negative height for top-down orientation.
272+ https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
273+ https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createdibsection
270274 """
271275 srcdc , memdc = self ._srcdc , self ._memdc
272276 gdi = self .gdi32
@@ -275,25 +279,40 @@ def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:
275279 if self ._region_width_height != (width , height ):
276280 self ._region_width_height = (width , height )
277281 self ._bmi .bmiHeader .biWidth = width
278- self ._bmi .bmiHeader .biHeight = - height # Why minus? See [1]
279- self ._data = ctypes .create_string_buffer (width * height * 4 ) # [2]
280- if self ._bmp :
281- gdi .DeleteObject (self ._bmp )
282- # Set to None to prevent another DeleteObject in case CreateCompatibleBitmap raises an exception.
283- self ._bmp = None
284- self ._bmp = gdi .CreateCompatibleBitmap (srcdc , width , height )
285- gdi .SelectObject (memdc , self ._bmp )
282+ self ._bmi .bmiHeader .biHeight = - height # Negative for top-down DIB
283+
284+ if self ._dib :
285+ gdi .DeleteObject (self ._dib )
286+ self ._dib = None
287+
288+ # CreateDIBSection creates the DIB and returns a pointer to the pixel data
289+ self ._dib_bits = LPVOID ()
290+ self ._dib = gdi .CreateDIBSection (
291+ memdc ,
292+ self ._bmi ,
293+ DIB_RGB_COLORS ,
294+ ctypes .byref (self ._dib_bits ),
295+ None , # hSection = NULL (system allocates memory)
296+ 0 , # offset = 0
297+ )
298+ gdi .SelectObject (memdc , self ._dib )
286299
300+ # Create a ctypes array type that maps directly to the DIB memory.
301+ # This avoids the overhead of ctypes.string_at() creating an intermediate bytes object.
302+ size = width * height * 4
303+ array_type = ctypes .c_char * size
304+ self ._dib_array = ctypes .cast (self ._dib_bits , POINTER (array_type )).contents
305+
306+ # BitBlt copies screen content directly into the DIB's memory
287307 gdi .BitBlt (memdc , 0 , 0 , width , height , srcdc , monitor ["left" ], monitor ["top" ], SRCCOPY | CAPTUREBLT )
288- assert self ._data is not None # noqa: S101 for type checker
289- scanlines_copied = gdi .GetDIBits (memdc , self ._bmp , 0 , height , self ._data , self ._bmi , DIB_RGB_COLORS )
290- if scanlines_copied != height :
291- # If the result was 0 (failure), an exception would have been raised by _errcheck. This is just a sanity
292- # clause.
293- msg = f"gdi32.GetDIBits() failed: only { scanlines_copied } scanlines copied instead of { height } "
294- raise ScreenShotError (msg )
295-
296- return self .cls_image (bytearray (self ._data ), monitor )
308+
309+ # Flush GDI operations to ensure DIB memory is fully updated before reading.
310+ # This ensures the BitBlt has completed before we access the memory.
311+ gdi .GdiFlush ()
312+
313+ # Read directly from DIB memory via the cached array view
314+ assert self ._dib_array is not None # noqa: S101 for type checker
315+ return self .cls_image (bytearray (self ._dib_array ), monitor )
297316
298317 def _cursor_impl (self ) -> ScreenShot | None :
299318 """Retrieve all cursor data. Pixels have to be RGB."""
0 commit comments