Skip to content

Commit afc1fc5

Browse files
authored
Add tests for stdole.IPicture creation through IStream. (#892)
* test: Prepare for `stdole.IPicture` interface testing. Imports `comtypes.gen.stdole` in `test_stream.py` and adds a new test class `Test_Picture` with a placeholder test `test_ole_load_picture`. This is a preliminary step to implement comprehensive testing for the `stdole.IPicture` interface. * test: Declare `OleLoadPicture` function in `test_stream.py`. This includes setting its argument types and return type using `ctypes` to allow for loading `stdole.IPicture` objects from a `IStream`. * test: Add `create_pixel_data` function for BMP image generation. Introduces a new function `create_pixel_data` in `test_stream.py` to generate 32-bit BGRA BMP binary data. This utility function allows for creating synthetic image data with specified dimensions, color, and DPI. * test: Add `OleLoadPicture` test for `stdole.IPicture` creation. This test validates the loading of a `stdole.IPicture` object from a stream containing bitmap data using `_OleLoadPicture`. It involves Windows API calls for device context, global memory management, and stream operations, ensuring the correct instantiation of the `IPicture` interface. * refactor: Enhance `_create_stream` for flexible `stdole.IPicture` testing. The `_create_stream` helper function in `test_stream.py` is refactored to accept `handle` and `delete_on_release` parameters. This allows for more controlled stream creation, particularly for testing scenarios involving `stdole.IPicture` where the underlying global memory management needs to be externalized. * test: Introduce `get_dc` context manager for `stdole.IPicture` testing. A new `get_dc` context manager is added to `test_stream.py` to encapsulate the acquisition and release of device contexts (DC) via `_GetDC` and `_ReleaseDC`. This improves the test for `stdole.IPicture` by: - Reducing the number of variables within the main test logic, enhancing readability. - Separating setup (DC acquisition) and teardown (DC release) processes from the core test assertions, making the test cleaner and more maintainable. * test: Add `global_alloc` and `global_lock` context managers for `stdole.IPicture` testing. Two new context managers, `global_alloc` and `global_lock`, are introduced in `test_stream.py`. These encapsulate `_GlobalAlloc`/`_GlobalFree` and `_GlobalLock`/`_GlobalUnlock` Windows API calls respectively. * test: Extract DPI retrieval into `get_screen_dpi` for `stdole.IPicture` testing. The logic for retrieving screen DPI is extracted into a new helper function, `get_screen_dpi`, in `test_stream.py`. This function utilizes the `get_dc` context manager to safely obtain and release the device context. * test: Verify `stdole.IPicture.Type` property after loading. This is a direct validation of the `stdole.IPicture` interface's behavior.
1 parent 86d39f5 commit afc1fc5

File tree

1 file changed

+216
-4
lines changed

1 file changed

+216
-4
lines changed

comtypes/test/test_stream.py

Lines changed: 216 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,43 @@
1+
import contextlib
2+
import ctypes
3+
import struct
14
import unittest as ut
2-
from ctypes import HRESULT, POINTER, OleDLL, byref, c_ubyte, c_ulonglong, pointer
3-
from ctypes.wintypes import BOOL, HGLOBAL, ULARGE_INTEGER
5+
from collections.abc import Iterator
6+
from ctypes import (
7+
HRESULT,
8+
POINTER,
9+
OleDLL,
10+
WinDLL,
11+
byref,
12+
c_size_t,
13+
c_ubyte,
14+
c_ulonglong,
15+
pointer,
16+
)
17+
from ctypes.wintypes import (
18+
BOOL,
19+
HDC,
20+
HGLOBAL,
21+
HWND,
22+
INT,
23+
LONG,
24+
LPVOID,
25+
UINT,
26+
ULARGE_INTEGER,
27+
)
28+
from typing import Optional
429

530
import comtypes.client
31+
from comtypes import hresult
632

733
comtypes.client.GetModule("portabledeviceapi.dll")
34+
# The stdole module is generated automatically during the portabledeviceapi
35+
# module generation.
36+
import comtypes.gen.stdole as stdole
837
from comtypes.gen.PortableDeviceApiLib import IStream
938

39+
SIZE_T = c_size_t
40+
1041
STATFLAG_DEFAULT = 0
1142
STGC_DEFAULT = 0
1243
STGTY_STREAM = 2
@@ -27,10 +58,12 @@
2758
_IStream_Size.restype = HRESULT
2859

2960

30-
def _create_stream() -> IStream:
61+
def _create_stream(
62+
handle: Optional[int] = None, delete_on_release: bool = True
63+
) -> IStream:
3164
# Create an IStream
3265
stream = POINTER(IStream)() # type: ignore
33-
_CreateStreamOnHGlobal(None, True, byref(stream))
66+
_CreateStreamOnHGlobal(handle, delete_on_release, byref(stream))
3467
return stream # type: ignore
3568

3669

@@ -158,5 +191,184 @@ def test_Clone(self):
158191
self.assertEqual(bytearray(buf)[0:read], test_data)
159192

160193

194+
_user32 = WinDLL("user32")
195+
196+
_GetDC = _user32.GetDC
197+
_GetDC.argtypes = (HWND,)
198+
_GetDC.restype = HDC
199+
200+
_ReleaseDC = _user32.ReleaseDC
201+
_ReleaseDC.argtypes = (HWND, HDC)
202+
_ReleaseDC.restype = INT
203+
204+
_gdi32 = WinDLL("gdi32")
205+
206+
_GetDeviceCaps = _gdi32.GetDeviceCaps
207+
_GetDeviceCaps.argtypes = (HDC, INT)
208+
_GetDeviceCaps.restype = INT
209+
210+
_kernel32 = WinDLL("kernel32")
211+
212+
_GlobalAlloc = _kernel32.GlobalAlloc
213+
_GlobalAlloc.argtypes = (UINT, SIZE_T)
214+
_GlobalAlloc.restype = HGLOBAL
215+
216+
_GlobalFree = _kernel32.GlobalFree
217+
_GlobalFree.argtypes = (HGLOBAL,)
218+
_GlobalFree.restype = HGLOBAL
219+
220+
_GlobalLock = _kernel32.GlobalLock
221+
_GlobalLock.argtypes = (HGLOBAL,)
222+
_GlobalLock.restype = LPVOID
223+
224+
_GlobalUnlock = _kernel32.GlobalUnlock
225+
_GlobalUnlock.argtypes = (HGLOBAL,)
226+
_GlobalUnlock.restype = BOOL
227+
228+
_oleaut32 = WinDLL("oleaut32")
229+
230+
_OleLoadPicture = _oleaut32.OleLoadPicture
231+
_OleLoadPicture.argtypes = (
232+
POINTER(IStream), # lpstm
233+
LONG, # lSize
234+
BOOL, # fSave
235+
POINTER(comtypes.GUID), # riid
236+
POINTER(POINTER(comtypes.IUnknown)), # ppvObj
237+
)
238+
_OleLoadPicture.restype = HRESULT
239+
240+
# Constants for the type of a picture object
241+
PICTYPE_BITMAP = 1
242+
243+
# Constants for GetDeviceCaps
244+
LOGPIXELSX = 88 # Logical pixels/inch in X
245+
LOGPIXELSY = 90 # Logical pixels/inch in Y
246+
247+
METERS_PER_INCH = 0.0254
248+
249+
GMEM_FIXED = 0x0000
250+
GMEM_ZEROINIT = 0x0040
251+
252+
BI_RGB = 0 # No compression
253+
254+
255+
@contextlib.contextmanager
256+
def get_dc(hwnd: int) -> Iterator[int]:
257+
"""Context manager to get and release a device context (DC)."""
258+
dc = _GetDC(hwnd)
259+
assert dc, "Failed to get device context."
260+
try:
261+
yield dc
262+
finally:
263+
# Release the device context
264+
_ReleaseDC(hwnd, dc)
265+
266+
267+
@contextlib.contextmanager
268+
def global_alloc(uflags: int, dwbytes: int) -> Iterator[int]:
269+
"""Context manager to allocate and free a global memory handle."""
270+
handle = _GlobalAlloc(uflags, dwbytes)
271+
assert handle, "Failed to GlobalAlloc"
272+
try:
273+
yield handle
274+
finally:
275+
_GlobalFree(handle)
276+
277+
278+
@contextlib.contextmanager
279+
def global_lock(handle: int) -> Iterator[int]:
280+
"""Context manager to lock a global memory handle and obtain a pointer."""
281+
lp_mem = _GlobalLock(handle)
282+
assert lp_mem, "Failed to GlobalLock"
283+
try:
284+
yield lp_mem
285+
finally:
286+
_GlobalUnlock(handle)
287+
288+
289+
def get_screen_dpi() -> tuple[int, int]:
290+
"""Gets the screen DPI using GDI functions."""
291+
# Get a handle to the desktop window's device context
292+
with get_dc(0) as dc:
293+
# Get the horizontal and vertical DPI
294+
dpi_x = _GetDeviceCaps(dc, LOGPIXELSX)
295+
dpi_y = _GetDeviceCaps(dc, LOGPIXELSY)
296+
return dpi_x, dpi_y
297+
298+
299+
def create_pixel_data(
300+
red: int,
301+
green: int,
302+
blue: int,
303+
dpi_x: int,
304+
dpi_y: int,
305+
width: int,
306+
height: int,
307+
) -> bytes:
308+
# Generates width x height pixel 32-bit BGRA BMP binary data.
309+
SIZEOF_BITMAPFILEHEADER = 14
310+
SIZEOF_BITMAPINFOHEADER = 40
311+
pixel_data = b""
312+
for _ in range(height):
313+
# Each row is padded to a 4-byte boundary. For 32bpp, no padding is needed.
314+
for _ in range(width):
315+
# B, G, R, Alpha (fully opaque)
316+
pixel_data += struct.pack(b"BBBB", blue, green, red, 0xFF)
317+
BITMAP_DATA_OFFSET = SIZEOF_BITMAPFILEHEADER + SIZEOF_BITMAPINFOHEADER
318+
file_size = BITMAP_DATA_OFFSET + len(pixel_data)
319+
bmp_header = struct.pack(
320+
b"<2sIHHI",
321+
b"BM", # File type signature "BM"
322+
file_size, # Total file size
323+
0, # Reserved1
324+
0, # Reserved2
325+
BITMAP_DATA_OFFSET, # Offset to pixel data
326+
)
327+
# Calculate pixels_per_meter based on the provided DPI
328+
pixels_per_meter_x = int(dpi_x / METERS_PER_INCH)
329+
pixels_per_meter_y = int(dpi_y / METERS_PER_INCH)
330+
info_header = struct.pack(
331+
b"<IiiHHIIiiII",
332+
SIZEOF_BITMAPINFOHEADER, # Size of BITMAPINFOHEADER
333+
width, # Image width
334+
height, # Image height
335+
1, # Planes
336+
32, # Bits per pixel (for BGRA)
337+
BI_RGB, # Compression
338+
len(pixel_data), # Size of image data
339+
pixels_per_meter_x, # X pixels per meter
340+
pixels_per_meter_y, # Y pixels per meter
341+
0, # Colors used
342+
0, # Colors important
343+
)
344+
return bmp_header + info_header + pixel_data
345+
346+
347+
class Test_Picture(ut.TestCase):
348+
def test_ole_load_picture(self):
349+
dpi_x, dpi_y = get_screen_dpi()
350+
data = create_pixel_data(255, 0, 0, dpi_x, dpi_y, 1, 1)
351+
# Allocate global memory with `GMEM_FIXED` (fixed-size) and
352+
# `GMEM_ZEROINIT` (initialize to zero) and copy BMP data.
353+
with global_alloc(GMEM_FIXED | GMEM_ZEROINIT, len(data)) as handle:
354+
with global_lock(handle) as lp_mem:
355+
ctypes.memmove(lp_mem, data, len(data))
356+
pstm = _create_stream(handle, delete_on_release=False)
357+
# Load picture from the stream
358+
pic: stdole.IPicture = POINTER(stdole.IPicture)() # type: ignore
359+
hr = _OleLoadPicture(
360+
pstm,
361+
len(data), # lSize
362+
False, # fSave
363+
byref(stdole.IPicture._iid_),
364+
byref(pic),
365+
)
366+
self.assertEqual(hr, hresult.S_OK)
367+
self.assertEqual(pic.Type, PICTYPE_BITMAP)
368+
pstm.RemoteSeek(0, STREAM_SEEK_SET)
369+
buf, read = pstm.RemoteRead(len(data))
370+
self.assertEqual(bytes(buf)[:read], data)
371+
372+
161373
if __name__ == "__main__":
162374
ut.main()

0 commit comments

Comments
 (0)