From 3f295bbdb2cb1b3a772afdffe5f7da56103dc379 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 29 Dec 2025 18:49:57 +0900 Subject: [PATCH 1/9] 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. --- comtypes/test/test_stream.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index 6c18df53..f12bff47 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -5,6 +5,9 @@ import comtypes.client comtypes.client.GetModule("portabledeviceapi.dll") +# The stdole module is generated automatically during the portabledeviceapi +# module generation. +import comtypes.gen.stdole as stdole from comtypes.gen.PortableDeviceApiLib import IStream STATFLAG_DEFAULT = 0 @@ -158,5 +161,10 @@ def test_Clone(self): self.assertEqual(bytearray(buf)[0:read], test_data) +class Test_Picture(ut.TestCase): + def test_ole_load_picture(self): + stdole.IPicture # TODO: Add test. + + if __name__ == "__main__": ut.main() From edeac4ce4b6656e792c05cdeaf70ed9fd4b875a0 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 29 Dec 2025 18:49:58 +0900 Subject: [PATCH 2/9] 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`. --- comtypes/test/test_stream.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index f12bff47..5b3f08a8 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -1,6 +1,15 @@ import unittest as ut -from ctypes import HRESULT, POINTER, OleDLL, byref, c_ubyte, c_ulonglong, pointer -from ctypes.wintypes import BOOL, HGLOBAL, ULARGE_INTEGER +from ctypes import ( + HRESULT, + POINTER, + OleDLL, + WinDLL, + byref, + c_ubyte, + c_ulonglong, + pointer, +) +from ctypes.wintypes import BOOL, HGLOBAL, LONG, ULARGE_INTEGER import comtypes.client @@ -161,6 +170,19 @@ def test_Clone(self): self.assertEqual(bytearray(buf)[0:read], test_data) +_oleaut32 = WinDLL("oleaut32") + +_OleLoadPicture = _oleaut32.OleLoadPicture +_OleLoadPicture.argtypes = ( + POINTER(IStream), # lpstm + LONG, # lSize + BOOL, # fSave + POINTER(comtypes.GUID), # riid + POINTER(POINTER(comtypes.IUnknown)), # ppvObj +) +_OleLoadPicture.restype = HRESULT + + class Test_Picture(ut.TestCase): def test_ole_load_picture(self): stdole.IPicture # TODO: Add test. From e0984fc19db45f669e795478eec97e7ff4aa327a Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 29 Dec 2025 18:49:58 +0900 Subject: [PATCH 3/9] 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. --- comtypes/test/test_stream.py | 54 ++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index 5b3f08a8..39b14813 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -1,3 +1,4 @@ +import struct import unittest as ut from ctypes import ( HRESULT, @@ -183,6 +184,59 @@ def test_Clone(self): _OleLoadPicture.restype = HRESULT +METERS_PER_INCH = 0.0254 + +BI_RGB = 0 # No compression + + +def create_pixel_data( + red: int, + green: int, + blue: int, + dpi_x: int, + dpi_y: int, + width: int, + height: int, +) -> bytes: + # Generates width x height pixel 32-bit BGRA BMP binary data. + SIZEOF_BITMAPFILEHEADER = 14 + SIZEOF_BITMAPINFOHEADER = 40 + pixel_data = b"" + for _ in range(height): + # Each row is padded to a 4-byte boundary. For 32bpp, no padding is needed. + for _ in range(width): + # B, G, R, Alpha (fully opaque) + pixel_data += struct.pack(b"BBBB", blue, green, red, 0xFF) + BITMAP_DATA_OFFSET = SIZEOF_BITMAPFILEHEADER + SIZEOF_BITMAPINFOHEADER + file_size = BITMAP_DATA_OFFSET + len(pixel_data) + bmp_header = struct.pack( + b"<2sIHHI", + b"BM", # File type signature "BM" + file_size, # Total file size + 0, # Reserved1 + 0, # Reserved2 + BITMAP_DATA_OFFSET, # Offset to pixel data + ) + # Calculate pixels_per_meter based on the provided DPI + pixels_per_meter_x = int(dpi_x / METERS_PER_INCH) + pixels_per_meter_y = int(dpi_y / METERS_PER_INCH) + info_header = struct.pack( + b" Date: Mon, 29 Dec 2025 18:49:58 +0900 Subject: [PATCH 4/9] 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. --- comtypes/test/test_stream.py | 92 +++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index 39b14813..93b2fbd6 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -1,3 +1,4 @@ +import ctypes import struct import unittest as ut from ctypes import ( @@ -6,13 +7,25 @@ OleDLL, WinDLL, byref, + c_size_t, c_ubyte, c_ulonglong, pointer, ) -from ctypes.wintypes import BOOL, HGLOBAL, LONG, ULARGE_INTEGER +from ctypes.wintypes import ( + BOOL, + HDC, + HGLOBAL, + HWND, + INT, + LONG, + LPVOID, + UINT, + ULARGE_INTEGER, +) import comtypes.client +from comtypes import hresult comtypes.client.GetModule("portabledeviceapi.dll") # The stdole module is generated automatically during the portabledeviceapi @@ -20,6 +33,8 @@ import comtypes.gen.stdole as stdole from comtypes.gen.PortableDeviceApiLib import IStream +SIZE_T = c_size_t + STATFLAG_DEFAULT = 0 STGC_DEFAULT = 0 STGTY_STREAM = 2 @@ -171,6 +186,40 @@ def test_Clone(self): self.assertEqual(bytearray(buf)[0:read], test_data) +_user32 = WinDLL("user32") + +_GetDC = _user32.GetDC +_GetDC.argtypes = (HWND,) +_GetDC.restype = HDC + +_ReleaseDC = _user32.ReleaseDC +_ReleaseDC.argtypes = (HWND, HDC) +_ReleaseDC.restype = INT + +_gdi32 = WinDLL("gdi32") + +_GetDeviceCaps = _gdi32.GetDeviceCaps +_GetDeviceCaps.argtypes = (HDC, INT) +_GetDeviceCaps.restype = INT + +_kernel32 = WinDLL("kernel32") + +_GlobalAlloc = _kernel32.GlobalAlloc +_GlobalAlloc.argtypes = (UINT, SIZE_T) +_GlobalAlloc.restype = HGLOBAL + +_GlobalFree = _kernel32.GlobalFree +_GlobalFree.argtypes = (HGLOBAL,) +_GlobalFree.restype = HGLOBAL + +_GlobalLock = _kernel32.GlobalLock +_GlobalLock.argtypes = (HGLOBAL,) +_GlobalLock.restype = LPVOID + +_GlobalUnlock = _kernel32.GlobalUnlock +_GlobalUnlock.argtypes = (HGLOBAL,) +_GlobalUnlock.restype = BOOL + _oleaut32 = WinDLL("oleaut32") _OleLoadPicture = _oleaut32.OleLoadPicture @@ -183,9 +232,15 @@ def test_Clone(self): ) _OleLoadPicture.restype = HRESULT +# Constants for GetDeviceCaps +LOGPIXELSX = 88 # Logical pixels/inch in X +LOGPIXELSY = 90 # Logical pixels/inch in Y METERS_PER_INCH = 0.0254 +GMEM_FIXED = 0x0000 +GMEM_ZEROINIT = 0x0040 + BI_RGB = 0 # No compression @@ -239,7 +294,40 @@ def create_pixel_data( class Test_Picture(ut.TestCase): def test_ole_load_picture(self): - stdole.IPicture # TODO: Add test. + try: + dc = _GetDC(0) + # Get the horizontal and vertical DPI + dpi_x = _GetDeviceCaps(dc, LOGPIXELSX) + dpi_y = _GetDeviceCaps(dc, LOGPIXELSY) + finally: + _ReleaseDC(0, dc) + data = create_pixel_data(255, 0, 0, dpi_x, dpi_y, 1, 1) + handle = _GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, len(data)) + assert handle, "Failed to GlobalAlloc" + try: + lp_mem = _GlobalLock(handle) + assert lp_mem, "Failed to GlobalLock" + try: + ctypes.memmove(lp_mem, data, len(data)) + finally: + _GlobalUnlock(lp_mem) + pstm = POINTER(IStream)() + _CreateStreamOnHGlobal(handle, False, byref(pstm)) + # Load picture from the stream + pic: stdole.IPicture = POINTER(stdole.IPicture)() # type: ignore + hr = _OleLoadPicture( + pstm, + len(data), # lSize + False, # fSave + byref(stdole.IPicture._iid_), + byref(pic), + ) + self.assertEqual(hr, hresult.S_OK) + pstm.RemoteSeek(0, STREAM_SEEK_SET) + buf, read = pstm.RemoteRead(len(data)) + finally: + _GlobalFree(handle) + self.assertEqual(bytes(buf)[:read], data) if __name__ == "__main__": From 2416f5095c8a5939ec37a6839d4f2aec902d352f Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 29 Dec 2025 18:49:58 +0900 Subject: [PATCH 5/9] 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. --- comtypes/test/test_stream.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index 93b2fbd6..82c43194 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -23,6 +23,7 @@ UINT, ULARGE_INTEGER, ) +from typing import Optional import comtypes.client from comtypes import hresult @@ -55,10 +56,12 @@ _IStream_Size.restype = HRESULT -def _create_stream() -> IStream: +def _create_stream( + handle: Optional[int] = None, delete_on_release: bool = True +) -> IStream: # Create an IStream stream = POINTER(IStream)() # type: ignore - _CreateStreamOnHGlobal(None, True, byref(stream)) + _CreateStreamOnHGlobal(handle, delete_on_release, byref(stream)) return stream # type: ignore @@ -311,8 +314,7 @@ def test_ole_load_picture(self): ctypes.memmove(lp_mem, data, len(data)) finally: _GlobalUnlock(lp_mem) - pstm = POINTER(IStream)() - _CreateStreamOnHGlobal(handle, False, byref(pstm)) + pstm = _create_stream(handle, delete_on_release=False) # Load picture from the stream pic: stdole.IPicture = POINTER(stdole.IPicture)() # type: ignore hr = _OleLoadPicture( From c87da259e6e3137032d5aef13a8d49e337124fdb Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 29 Dec 2025 18:49:58 +0900 Subject: [PATCH 6/9] 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. --- comtypes/test/test_stream.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index 82c43194..56a12218 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -1,6 +1,8 @@ +import contextlib import ctypes import struct import unittest as ut +from collections.abc import Iterator from ctypes import ( HRESULT, POINTER, @@ -247,6 +249,18 @@ def test_Clone(self): BI_RGB = 0 # No compression +@contextlib.contextmanager +def get_dc(hwnd: int) -> Iterator[int]: + """Context manager to get and release a device context (DC).""" + dc = _GetDC(hwnd) + assert dc, "Failed to get device context." + try: + yield dc + finally: + # Release the device context + _ReleaseDC(hwnd, dc) + + def create_pixel_data( red: int, green: int, @@ -297,13 +311,10 @@ def create_pixel_data( class Test_Picture(ut.TestCase): def test_ole_load_picture(self): - try: - dc = _GetDC(0) - # Get the horizontal and vertical DPI + # Get a handle to the desktop window's device context + with get_dc(0) as dc: dpi_x = _GetDeviceCaps(dc, LOGPIXELSX) dpi_y = _GetDeviceCaps(dc, LOGPIXELSY) - finally: - _ReleaseDC(0, dc) data = create_pixel_data(255, 0, 0, dpi_x, dpi_y, 1, 1) handle = _GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, len(data)) assert handle, "Failed to GlobalAlloc" From 0e4801cf824cc0734a6d2231e5a04ab75c174641 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 29 Dec 2025 18:49:58 +0900 Subject: [PATCH 7/9] 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. --- comtypes/test/test_stream.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index 56a12218..e3f929f3 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -261,6 +261,28 @@ def get_dc(hwnd: int) -> Iterator[int]: _ReleaseDC(hwnd, dc) +@contextlib.contextmanager +def global_alloc(uflags: int, dwbytes: int) -> Iterator[int]: + """Context manager to allocate and free a global memory handle.""" + handle = _GlobalAlloc(uflags, dwbytes) + assert handle, "Failed to GlobalAlloc" + try: + yield handle + finally: + _GlobalFree(handle) + + +@contextlib.contextmanager +def global_lock(handle: int) -> Iterator[int]: + """Context manager to lock a global memory handle and obtain a pointer.""" + lp_mem = _GlobalLock(handle) + assert lp_mem, "Failed to GlobalLock" + try: + yield lp_mem + finally: + _GlobalUnlock(handle) + + def create_pixel_data( red: int, green: int, @@ -316,15 +338,11 @@ def test_ole_load_picture(self): dpi_x = _GetDeviceCaps(dc, LOGPIXELSX) dpi_y = _GetDeviceCaps(dc, LOGPIXELSY) data = create_pixel_data(255, 0, 0, dpi_x, dpi_y, 1, 1) - handle = _GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, len(data)) - assert handle, "Failed to GlobalAlloc" - try: - lp_mem = _GlobalLock(handle) - assert lp_mem, "Failed to GlobalLock" - try: + # Allocate global memory with `GMEM_FIXED` (fixed-size) and + # `GMEM_ZEROINIT` (initialize to zero) and copy BMP data. + with global_alloc(GMEM_FIXED | GMEM_ZEROINIT, len(data)) as handle: + with global_lock(handle) as lp_mem: ctypes.memmove(lp_mem, data, len(data)) - finally: - _GlobalUnlock(lp_mem) pstm = _create_stream(handle, delete_on_release=False) # Load picture from the stream pic: stdole.IPicture = POINTER(stdole.IPicture)() # type: ignore @@ -338,8 +356,6 @@ def test_ole_load_picture(self): self.assertEqual(hr, hresult.S_OK) pstm.RemoteSeek(0, STREAM_SEEK_SET) buf, read = pstm.RemoteRead(len(data)) - finally: - _GlobalFree(handle) self.assertEqual(bytes(buf)[:read], data) From acecbfa8395d80fcd98c055d682704bd2c173e26 Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 29 Dec 2025 18:49:58 +0900 Subject: [PATCH 8/9] 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. --- comtypes/test/test_stream.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index e3f929f3..a11ed45e 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -283,6 +283,16 @@ def global_lock(handle: int) -> Iterator[int]: _GlobalUnlock(handle) +def get_screen_dpi() -> tuple[int, int]: + """Gets the screen DPI using GDI functions.""" + # Get a handle to the desktop window's device context + with get_dc(0) as dc: + # Get the horizontal and vertical DPI + dpi_x = _GetDeviceCaps(dc, LOGPIXELSX) + dpi_y = _GetDeviceCaps(dc, LOGPIXELSY) + return dpi_x, dpi_y + + def create_pixel_data( red: int, green: int, @@ -333,10 +343,7 @@ def create_pixel_data( class Test_Picture(ut.TestCase): def test_ole_load_picture(self): - # Get a handle to the desktop window's device context - with get_dc(0) as dc: - dpi_x = _GetDeviceCaps(dc, LOGPIXELSX) - dpi_y = _GetDeviceCaps(dc, LOGPIXELSY) + dpi_x, dpi_y = get_screen_dpi() data = create_pixel_data(255, 0, 0, dpi_x, dpi_y, 1, 1) # Allocate global memory with `GMEM_FIXED` (fixed-size) and # `GMEM_ZEROINIT` (initialize to zero) and copy BMP data. From d56f9c647b9eb1cb80ad3855268dca272f42e89a Mon Sep 17 00:00:00 2001 From: junkmd Date: Mon, 29 Dec 2025 18:49:58 +0900 Subject: [PATCH 9/9] test: Verify `stdole.IPicture.Type` property after loading. This is a direct validation of the `stdole.IPicture` interface's behavior. --- comtypes/test/test_stream.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index a11ed45e..006140bc 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -237,6 +237,9 @@ def test_Clone(self): ) _OleLoadPicture.restype = HRESULT +# Constants for the type of a picture object +PICTYPE_BITMAP = 1 + # Constants for GetDeviceCaps LOGPIXELSX = 88 # Logical pixels/inch in X LOGPIXELSY = 90 # Logical pixels/inch in Y @@ -361,6 +364,7 @@ def test_ole_load_picture(self): byref(pic), ) self.assertEqual(hr, hresult.S_OK) + self.assertEqual(pic.Type, PICTYPE_BITMAP) pstm.RemoteSeek(0, STREAM_SEEK_SET) buf, read = pstm.RemoteRead(len(data)) self.assertEqual(bytes(buf)[:read], data)