From 3afca0456dc4d363817db1fea9c802b4a29fda87 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 30 Dec 2025 08:54:54 +0900 Subject: [PATCH 01/10] test: Simplify BMP generation in `test_stream.py` by removing DPI calculation. Removed GDI-based DPI calculation (`get_screen_dpi`, `_GetDeviceCaps`, etc.) because the DPI was fixed to 0, eliminating the need to determine screen resolution and thereby streamlining the BMP generation logic. The `create_pixel_data` function was renamed to `create_24bit_pixel_data`. It now exclusively generates 24-bit BGR pixel data with 0 DPI, ensuring consistent bitmap output independent of system settings. This includes the addition of row padding for 24-bit BMP format compliance. --- comtypes/test/test_stream.py | 57 +++++++++++++----------------------- 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index 006140bc..ed258d21 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -201,12 +201,6 @@ def test_Clone(self): _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 @@ -240,12 +234,6 @@ def test_Clone(self): # 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 - -METERS_PER_INCH = 0.0254 - GMEM_FIXED = 0x0000 GMEM_ZEROINIT = 0x0040 @@ -286,34 +274,28 @@ 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( +def create_24bit_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. + # Generates width x height pixel 24-bit BGR BMP binary data with 0 DPI. 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. + # Each row is padded to a 4-byte boundary. + # For 24bpp, each pixel is 3 bytes (BGR). + # Row size without padding: width * 3 bytes + row_size = width * 3 + # Calculate padding bytes (to make row_size a multiple of 4) + padding_bytes = (4 - (row_size % 4)) % 4 for _ in range(width): - # B, G, R, Alpha (fully opaque) - pixel_data += struct.pack(b"BBBB", blue, green, red, 0xFF) + # B, G, R + pixel_data += struct.pack(b"BBB", blue, green, red) + pixel_data += b"\x00" * padding_bytes BITMAP_DATA_OFFSET = SIZEOF_BITMAPFILEHEADER + SIZEOF_BITMAPINFOHEADER file_size = BITMAP_DATA_OFFSET + len(pixel_data) bmp_header = struct.pack( @@ -324,20 +306,22 @@ def create_pixel_data( 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: Tue, 30 Dec 2025 08:54:54 +0900 Subject: [PATCH 02/10] test: Use variables for width and height in `create_24bit_pixel_data` call. In `test_stream.py`, within `test_ole_load_picture`, the `width` and `height` arguments passed to the `create_24bit_pixel_data` function were replaced from direct literals with dedicated variables. This change enhances readability by explicitly naming the dimensions, improving the clarity of the test's intent. --- comtypes/test/test_stream.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index ed258d21..05d8f468 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -330,7 +330,8 @@ def create_24bit_pixel_data( class Test_Picture(ut.TestCase): def test_ole_load_picture(self): - data = create_24bit_pixel_data(255, 0, 0, 1, 1) # 1x1 pixel red picture + width, height = 1, 1 + data = create_24bit_pixel_data(255, 0, 0, width, height) # Red pixel # 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: From 5e55e6b4331d36ba55075c3c7a63548a31c1dea6 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 30 Dec 2025 08:54:54 +0900 Subject: [PATCH 03/10] test: Add `test_save_created_bitmap_picture` for OLE picture saving. Introduces a new test case `test_save_created_bitmap_picture` in `test_stream.py` to verify the saving of OLE picture objects created from bitmaps. --- comtypes/test/test_stream.py | 214 +++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index 05d8f468..f6afb548 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -7,16 +7,21 @@ HRESULT, POINTER, OleDLL, + Structure, WinDLL, byref, c_size_t, c_ubyte, c_ulonglong, + c_void_p, pointer, ) from ctypes.wintypes import ( BOOL, + DWORD, + HANDLE, HDC, + HGDIOBJ, HGLOBAL, HWND, INT, @@ -24,6 +29,7 @@ LPVOID, UINT, ULARGE_INTEGER, + WORD, ) from typing import Optional @@ -201,6 +207,59 @@ def test_Clone(self): _ReleaseDC.argtypes = (HWND, HDC) _ReleaseDC.restype = INT +_gdi32 = WinDLL("gdi32") + +_CreateCompatibleDC = _gdi32.CreateCompatibleDC +_CreateCompatibleDC.argtypes = (HDC,) +_CreateCompatibleDC.restype = HDC + +_DeleteDC = _gdi32.DeleteDC +_DeleteDC.argtypes = (HDC,) +_DeleteDC.restype = BOOL + +_SelectObject = _gdi32.SelectObject +_SelectObject.argtypes = (HDC, HGDIOBJ) +_SelectObject.restype = HGDIOBJ + +_DeleteObject = _gdi32.DeleteObject +_DeleteObject.argtypes = (HGDIOBJ,) +_DeleteObject.restype = BOOL + + +class BITMAPINFOHEADER(Structure): + _fields_ = [ + ("biSize", DWORD), + ("biWidth", LONG), + ("biHeight", LONG), + ("biPlanes", WORD), + ("biBitCount", WORD), + ("biCompression", DWORD), + ("biSizeImage", DWORD), + ("biXPelsPerMeter", LONG), + ("biYPelsPerMeter", LONG), + ("biClrUsed", DWORD), + ("biClrImportant", DWORD), + ] + + +class BITMAPINFO(Structure): + _fields_ = [ + ("bmiHeader", BITMAPINFOHEADER), + ("bmiColors", DWORD * 1), # Placeholder for color table, not used for 32bpp + ] + + +_CreateDIBSection = _gdi32.CreateDIBSection +_CreateDIBSection.argtypes = ( + HDC, + POINTER(BITMAPINFO), + UINT, # DIB_RGB_COLORS + POINTER(c_void_p), # lplpBits + HANDLE, # hSection + DWORD, # dwOffset +) +_CreateDIBSection.restype = HGDIOBJ + _kernel32 = WinDLL("kernel32") _GlobalAlloc = _kernel32.GlobalAlloc @@ -231,6 +290,82 @@ def test_Clone(self): ) _OleLoadPicture.restype = HRESULT + +class _PICTDESC_BMP(Structure): + _fields_ = [ + ("hbitmap", HGDIOBJ), + ("hpal", DWORD), # COLORREF is DWORD + ] + + +class _PICTDESC_WMF(Structure): + _fields_ = [ + ("hmetafile", HGDIOBJ), # HMETAFILE + ("xExt", INT), + ("yExt", INT), + ] + + +class _PICTDESC_EMF(Structure): + _fields_ = [ + ("hemf", HGDIOBJ), # HENHMETAFILE + ] + + +class _PICTDESC_ICON(Structure): + _fields_ = [ + ("hicon", HGDIOBJ), # HICON + ] + + +class _PICTDESC_CUR(Structure): + _fields_ = [ + ("hcur", HGDIOBJ), # HCURSOR + ] + + +class _PICTDESC_DISP(Structure): + _fields_ = [ + ("lpPicDisp", c_void_p), # LPPICTUREDISP + ] + + +class _PICTDESC_SIZE(Structure): + _fields_ = [ + ("cx", c_size_t), + ("cy", c_size_t), + ] + + +class PICTDESC_UNION(ctypes.Union): + _fields_ = [ + ("bmp", _PICTDESC_BMP), + ("wmf", _PICTDESC_WMF), + ("emf", _PICTDESC_EMF), + ("icon", _PICTDESC_ICON), + ("cur", _PICTDESC_CUR), + ("disp", _PICTDESC_DISP), + ("size", _PICTDESC_SIZE), + ] + + +class PICTDESC(Structure): + _fields_ = [ + ("cbSizeofstruct", UINT), + ("picType", UINT), + ("u", PICTDESC_UNION), + ] + + +_OleCreatePictureIndirect = _oleaut32.OleCreatePictureIndirect +_OleCreatePictureIndirect.argtypes = [ + POINTER(PICTDESC), # lpPictDesc + POINTER(comtypes.GUID), # riid + BOOL, # fOwn + POINTER(POINTER(comtypes.IUnknown)), # ppvObj +] +_OleCreatePictureIndirect.restype = HRESULT + # Constants for the type of a picture object PICTYPE_BITMAP = 1 @@ -238,6 +373,7 @@ def test_Clone(self): GMEM_ZEROINIT = 0x0040 BI_RGB = 0 # No compression +DIB_RGB_COLORS = 0 @contextlib.contextmanager @@ -274,6 +410,20 @@ def global_lock(handle: int) -> Iterator[int]: _GlobalUnlock(handle) +def create_24bitmap_info(width: int, height: int) -> BITMAPINFO: + """Creates a BITMAPINFO structure for a 24bpp BGR DIB section.""" + bmi = BITMAPINFO() + bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER) + bmi.bmiHeader.biWidth = width + bmi.bmiHeader.biHeight = height # positive for bottom-up DIB + bmi.bmiHeader.biPlanes = 1 + bmi.bmiHeader.biBitCount = 24 + bmi.bmiHeader.biCompression = BI_RGB + # width*height pixels * 3 bytes/pixel (BGR) + bmi.bmiHeader.biSizeImage = width * height * 3 + return bmi + + def create_24bit_pixel_data( red: int, green: int, @@ -353,6 +503,70 @@ def test_ole_load_picture(self): buf, read = pstm.RemoteRead(len(data)) self.assertEqual(bytes(buf)[:read], data) + def test_save_created_bitmap_picture(self): + # BGR, 1x1 pixel, blue (0, 0, 255), in Windows GDI. + # This is the data that will be directly copied into the DIB section's memory. + blue_pixel_data = b"\xff\x00\x00" # Blue (0, 0, 255) + width, height = 1, 1 + try: + screen_dc = _GetDC(0) + assert screen_dc, "Failed to get device context." + try: + mem_dc = _CreateCompatibleDC(screen_dc) + assert mem_dc, "Failed to create compatible memory DC." + bits = c_void_p() + bmi = create_24bitmap_info(width, height) + try: + hbm = _CreateDIBSection( + mem_dc, + byref(bmi), + DIB_RGB_COLORS, + byref(bits), + 0, + 0, + ) + assert hbm, "Failed to create DIB section." + try: + old_obj = _SelectObject(mem_dc, hbm) + assert old_obj, "Failed to select object into DC." + # Copy the raw pixel data into the DIB section's memory + ctypes.memmove(bits, blue_pixel_data, len(blue_pixel_data)) + # Populate PICTDESC with the HBITMAP from DIB section + pdesc = PICTDESC() + pdesc.cbSizeofstruct = ctypes.sizeof(PICTDESC) + pdesc.picType = PICTYPE_BITMAP + pdesc.u.bmp.hbitmap = hbm + pdesc.u.bmp.hpal = 0 # No palette for 24bpp + # Create IPicture using _OleCreatePictureIndirect + pic: stdole.IPicture = POINTER(stdole.IPicture)() # type: ignore + hr = _OleCreatePictureIndirect( + byref(pdesc), + byref(stdole.IPicture._iid_), + True, # fOwn: If True, the picture object owns the GDI handle. + byref(pic), + ) + self.assertEqual(hr, hresult.S_OK) + self.assertEqual(pic.Type, PICTYPE_BITMAP) + dststm = _create_stream(delete_on_release=True) + pic.SaveAsFile(dststm, True) + dststm.RemoteSeek(0, STREAM_SEEK_SET) + buf, read = dststm.RemoteRead( + dststm.Stat(STATFLAG_DEFAULT).cbSize + ) + finally: + _SelectObject(mem_dc, old_obj) + finally: + _DeleteObject(hbm) + finally: + _DeleteDC(mem_dc) + finally: + _ReleaseDC(0, screen_dc) + # Save picture to the stream + self.assertEqual( + bytes(buf)[:read], + create_24bit_pixel_data(0, 0, 255, width, height), + ) + if __name__ == "__main__": ut.main() From 81b8b804c719ceba12b0d2afeb3d52c3a109e7cf Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 30 Dec 2025 08:54:54 +0900 Subject: [PATCH 04/10] refactor: Use `get_dc` context manager in `test_save_created_bitmap_picture`. Refactors the `test_save_created_bitmap_picture` in `test_stream.py` to utilize the `get_dc` context manager for acquiring and releasing device contexts. This change improves code readability and ensures proper resource management within the test. --- comtypes/test/test_stream.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index f6afb548..86473cea 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -508,9 +508,7 @@ def test_save_created_bitmap_picture(self): # This is the data that will be directly copied into the DIB section's memory. blue_pixel_data = b"\xff\x00\x00" # Blue (0, 0, 255) width, height = 1, 1 - try: - screen_dc = _GetDC(0) - assert screen_dc, "Failed to get device context." + with get_dc(0) as screen_dc: try: mem_dc = _CreateCompatibleDC(screen_dc) assert mem_dc, "Failed to create compatible memory DC." @@ -559,8 +557,6 @@ def test_save_created_bitmap_picture(self): _DeleteObject(hbm) finally: _DeleteDC(mem_dc) - finally: - _ReleaseDC(0, screen_dc) # Save picture to the stream self.assertEqual( bytes(buf)[:read], From 643e77980a2d9e5baa86d3620a10fa7d0e00abb8 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 30 Dec 2025 08:54:54 +0900 Subject: [PATCH 05/10] refactor: Use `select_object` context manager for GDI object handling Introduces a `select_object` context manager to encapsulate the selection and restoration of GDI objects in a device context. --- comtypes/test/test_stream.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index 86473cea..40f185e6 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -410,6 +410,19 @@ def global_lock(handle: int) -> Iterator[int]: _GlobalUnlock(handle) +@contextlib.contextmanager +def select_object(hdc: int, obj: int) -> Iterator[int]: + """Context manager to select a GDI object into a device context and restore + the original. + """ + old_obj = _SelectObject(hdc, obj) + assert old_obj, "Failed to select object into DC." + try: + yield obj + finally: + _SelectObject(hdc, old_obj) + + def create_24bitmap_info(width: int, height: int) -> BITMAPINFO: """Creates a BITMAPINFO structure for a 24bpp BGR DIB section.""" bmi = BITMAPINFO() @@ -524,9 +537,7 @@ def test_save_created_bitmap_picture(self): 0, ) assert hbm, "Failed to create DIB section." - try: - old_obj = _SelectObject(mem_dc, hbm) - assert old_obj, "Failed to select object into DC." + with select_object(mem_dc, hbm): # Copy the raw pixel data into the DIB section's memory ctypes.memmove(bits, blue_pixel_data, len(blue_pixel_data)) # Populate PICTDESC with the HBITMAP from DIB section @@ -551,8 +562,6 @@ def test_save_created_bitmap_picture(self): buf, read = dststm.RemoteRead( dststm.Stat(STATFLAG_DEFAULT).cbSize ) - finally: - _SelectObject(mem_dc, old_obj) finally: _DeleteObject(hbm) finally: From 8673295b49d7d082e935b3afc6ec1a2cd537fa76 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 30 Dec 2025 08:54:54 +0900 Subject: [PATCH 06/10] refactor: Use `create_compatible_dc` context manager for GDI DC handling Introduces a `create_compatible_dc` context manager to encapsulate the creation and deletion of compatible device contexts. --- comtypes/test/test_stream.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index 40f185e6..5b4f0efe 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -410,6 +410,17 @@ def global_lock(handle: int) -> Iterator[int]: _GlobalUnlock(handle) +@contextlib.contextmanager +def create_compatible_dc(hdc: int) -> Iterator[int]: + """Context manager to create and delete a compatible device context.""" + mem_dc = _CreateCompatibleDC(hdc) + assert mem_dc, "Failed to create compatible memory DC." + try: + yield mem_dc + finally: + _DeleteDC(mem_dc) + + @contextlib.contextmanager def select_object(hdc: int, obj: int) -> Iterator[int]: """Context manager to select a GDI object into a device context and restore @@ -522,9 +533,7 @@ def test_save_created_bitmap_picture(self): blue_pixel_data = b"\xff\x00\x00" # Blue (0, 0, 255) width, height = 1, 1 with get_dc(0) as screen_dc: - try: - mem_dc = _CreateCompatibleDC(screen_dc) - assert mem_dc, "Failed to create compatible memory DC." + with create_compatible_dc(screen_dc) as mem_dc: bits = c_void_p() bmi = create_24bitmap_info(width, height) try: @@ -564,8 +573,6 @@ def test_save_created_bitmap_picture(self): ) finally: _DeleteObject(hbm) - finally: - _DeleteDC(mem_dc) # Save picture to the stream self.assertEqual( bytes(buf)[:read], From 5137a12b44d6297cdc3901d23906f09f90fd1266 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 30 Dec 2025 08:54:54 +0900 Subject: [PATCH 07/10] refactor: Use `create_image_rendering_dc` context manager for GDI rendering Introduces a `create_image_rendering_dc` context manager to encapsulate the creation and management of a device context for off-screen image rendering. This refactors `test_ole_load_picture` to utilize the new context manager, improving resource handling and test clarity. --- comtypes/test/test_stream.py | 115 ++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 41 deletions(-) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index 5b4f0efe..10bc1ea3 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -502,6 +502,57 @@ def create_24bit_pixel_data( return bmp_header + info_header + pixel_data +@contextlib.contextmanager +def create_image_rendering_dc( + hwnd: int, + width: int, + height: int, + usage: int = DIB_RGB_COLORS, + hsection: int = 0, + dwoffset: int = 0, +) -> Iterator[tuple[int, c_void_p, BITMAPINFO, int]]: + """Context manager to create a device context for off-screen image rendering. + + This sets up a memory device context (DC) with a DIB section, allowing + GDI operations to render into a memory buffer. + + Args: + hwnd: Handle to the window (0 for desktop). + width: Width of the image buffer. + height: Height of the image buffer. + usage: The type of DIB. Default is DIB_RGB_COLORS (0). + hsection: A handle to a file-mapping object. If NULL (0), the system + allocates memory for the DIB. + dwoffset: The offset from the beginning of the file-mapping object + specified by `hsection` to where the DIB bitmap begins. + + Yields: + A tuple containing: + - mem_dc: The handle to the memory device context. + - bits: Pointer to the pixel data of the DIB section. + - bmi: The structure describing the DIB section. + - hbm: The handle to the created DIB section bitmap. + """ + # Get a screen DC to use as a reference for creating a compatible DC + with get_dc(hwnd) as screen_dc, create_compatible_dc(screen_dc) as mem_dc: + bits = c_void_p() + bmi = create_24bitmap_info(width, height) + try: + hbm = _CreateDIBSection( + mem_dc, + byref(bmi), + usage, + byref(bits), + hsection, + dwoffset, + ) + assert hbm, "Failed to create DIB section." + with select_object(mem_dc, hbm): + yield mem_dc, bits, bmi, hbm + finally: + _DeleteObject(hbm) + + class Test_Picture(ut.TestCase): def test_ole_load_picture(self): width, height = 1, 1 @@ -532,47 +583,29 @@ def test_save_created_bitmap_picture(self): # This is the data that will be directly copied into the DIB section's memory. blue_pixel_data = b"\xff\x00\x00" # Blue (0, 0, 255) width, height = 1, 1 - with get_dc(0) as screen_dc: - with create_compatible_dc(screen_dc) as mem_dc: - bits = c_void_p() - bmi = create_24bitmap_info(width, height) - try: - hbm = _CreateDIBSection( - mem_dc, - byref(bmi), - DIB_RGB_COLORS, - byref(bits), - 0, - 0, - ) - assert hbm, "Failed to create DIB section." - with select_object(mem_dc, hbm): - # Copy the raw pixel data into the DIB section's memory - ctypes.memmove(bits, blue_pixel_data, len(blue_pixel_data)) - # Populate PICTDESC with the HBITMAP from DIB section - pdesc = PICTDESC() - pdesc.cbSizeofstruct = ctypes.sizeof(PICTDESC) - pdesc.picType = PICTYPE_BITMAP - pdesc.u.bmp.hbitmap = hbm - pdesc.u.bmp.hpal = 0 # No palette for 24bpp - # Create IPicture using _OleCreatePictureIndirect - pic: stdole.IPicture = POINTER(stdole.IPicture)() # type: ignore - hr = _OleCreatePictureIndirect( - byref(pdesc), - byref(stdole.IPicture._iid_), - True, # fOwn: If True, the picture object owns the GDI handle. - byref(pic), - ) - self.assertEqual(hr, hresult.S_OK) - self.assertEqual(pic.Type, PICTYPE_BITMAP) - dststm = _create_stream(delete_on_release=True) - pic.SaveAsFile(dststm, True) - dststm.RemoteSeek(0, STREAM_SEEK_SET) - buf, read = dststm.RemoteRead( - dststm.Stat(STATFLAG_DEFAULT).cbSize - ) - finally: - _DeleteObject(hbm) + with create_image_rendering_dc(0, width, height) as (_, bits, _, hbm): + # Copy the raw pixel data into the DIB section's memory + ctypes.memmove(bits, blue_pixel_data, len(blue_pixel_data)) + # Populate PICTDESC with the HBITMAP from DIB section + pdesc = PICTDESC() + pdesc.cbSizeofstruct = ctypes.sizeof(PICTDESC) + pdesc.picType = PICTYPE_BITMAP + pdesc.u.bmp.hbitmap = hbm + pdesc.u.bmp.hpal = 0 # No palette for 24bpp + # Create IPicture using _OleCreatePictureIndirect + pic: stdole.IPicture = POINTER(stdole.IPicture)() # type: ignore + hr = _OleCreatePictureIndirect( + byref(pdesc), + byref(stdole.IPicture._iid_), + True, # fOwn: If True, the picture object owns the GDI handle. + byref(pic), + ) + self.assertEqual(hr, hresult.S_OK) + self.assertEqual(pic.Type, PICTYPE_BITMAP) + dststm = _create_stream(delete_on_release=True) + pic.SaveAsFile(dststm, True) + dststm.RemoteSeek(0, STREAM_SEEK_SET) + buf, read = dststm.RemoteRead(dststm.Stat(STATFLAG_DEFAULT).cbSize) # Save picture to the stream self.assertEqual( bytes(buf)[:read], From 78b7f8fc3dba296fb6d71c545bf1bfc68f6ad779 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 30 Dec 2025 08:54:54 +0900 Subject: [PATCH 08/10] test: Add `test_load_from_buffer_stream` for OLE picture loading and saving. Introduces a new test case `test_load_from_buffer_stream` in `test_stream.py`. This test verifies the functionality of loading an OLE picture from a `bytearray` stream, rendering it into a GDI device context, and then saving it back to a stream, ensuring data integrity throughout the process. --- comtypes/test/test_stream.py | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index 10bc1ea3..b6d3af95 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -578,6 +578,45 @@ def test_ole_load_picture(self): buf, read = pstm.RemoteRead(len(data)) self.assertEqual(bytes(buf)[:read], data) + def test_load_from_buffer_stream(self): + width, height = 1, 1 + data = create_24bit_pixel_data(0, 255, 0, width, height) # Green pixel + srcstm = _create_stream(delete_on_release=True) + pv = (c_ubyte * len(data)).from_buffer(bytearray(data)) + srcstm.RemoteWrite(pv, len(data)) + srcstm.Commit(STGC_DEFAULT) + srcstm.RemoteSeek(0, STREAM_SEEK_SET) + # Load picture from the stream + pic: stdole.IPicture = POINTER(stdole.IPicture)() # type: ignore + hr = _OleLoadPicture( + srcstm, len(data), False, byref(stdole.IPicture._iid_), byref(pic) + ) + self.assertEqual(hr, hresult.S_OK) + self.assertEqual(pic.Type, PICTYPE_BITMAP) + with create_image_rendering_dc(0, width, height) as (mem_dc, bits, bmi, _): + pic.Render( + mem_dc, + 0, + 0, + 1, + 1, + 0, + pic.Height, # start from bottom for bottom-up DIB + pic.Width, + -pic.Height, # negative for top-down rendering in memory + None, + ) + # Read the pixel data directly from the bits pointer. + gdi_data = ctypes.string_at(bits, bmi.bmiHeader.biSizeImage) + # BGR, 1x1 pixel, green (0, 255, 0), in Windows GDI. + self.assertEqual(gdi_data, b"\x00\xff\x00") + # Save picture to the stream + dststm = _create_stream(delete_on_release=True) + pic.SaveAsFile(dststm, False) + dststm.RemoteSeek(0, STREAM_SEEK_SET) + buf, read = dststm.RemoteRead(dststm.Stat(STATFLAG_DEFAULT).cbSize) + self.assertEqual(bytes(buf)[:read], data) + def test_save_created_bitmap_picture(self): # BGR, 1x1 pixel, blue (0, 0, 255), in Windows GDI. # This is the data that will be directly copied into the DIB section's memory. From dde465c2abba0be48280804656783dd783c94010 Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 30 Dec 2025 08:54:54 +0900 Subject: [PATCH 09/10] fix: Ensure GDI operations are flushed before reading pixel data. Introduces a call to `_GdiFlush()` after `pic.Render()` in `test_load_from_buffer_stream` within `comtypes/test/test_stream.py`. This ensures that all GDI drawing commands are executed and memory is updated before attempting to read pixel data, preventing potential data inconsistencies. --- comtypes/test/test_stream.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index b6d3af95..1e15d663 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -225,6 +225,10 @@ def test_Clone(self): _DeleteObject.argtypes = (HGDIOBJ,) _DeleteObject.restype = BOOL +_GdiFlush = _gdi32.GdiFlush +_GdiFlush.argtypes = [] +_GdiFlush.restype = BOOL + class BITMAPINFOHEADER(Structure): _fields_ = [ @@ -606,6 +610,9 @@ def test_load_from_buffer_stream(self): -pic.Height, # negative for top-down rendering in memory None, ) + # Flush GDI operations to ensure all drawing commands are executed + # and memory is updated before reading. + _GdiFlush() # Read the pixel data directly from the bits pointer. gdi_data = ctypes.string_at(bits, bmi.bmiHeader.biSizeImage) # BGR, 1x1 pixel, green (0, 255, 0), in Windows GDI. From d64742a7c7ed2e1e2bdff57e6cc984cb46d14b5c Mon Sep 17 00:00:00 2001 From: junkmd Date: Tue, 30 Dec 2025 08:54:54 +0900 Subject: [PATCH 10/10] refactor: Rename `test_ole_load_picture` to `test_load_from_handle_stream`. Renames the test method `test_ole_load_picture` to `test_load_from_handle_stream` in `comtypes/test/test_stream.py`. This change provides a more descriptive and accurate name for the test, reflecting its functionality of loading an OLE picture from a handle- based stream. --- comtypes/test/test_stream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comtypes/test/test_stream.py b/comtypes/test/test_stream.py index 1e15d663..d6b3b6bb 100644 --- a/comtypes/test/test_stream.py +++ b/comtypes/test/test_stream.py @@ -558,7 +558,7 @@ def create_image_rendering_dc( class Test_Picture(ut.TestCase): - def test_ole_load_picture(self): + def test_load_from_handle_stream(self): width, height = 1, 1 data = create_24bit_pixel_data(255, 0, 0, width, height) # Red pixel # Allocate global memory with `GMEM_FIXED` (fixed-size) and