Last active
October 21, 2025 13:51
-
-
Save 59de44955ebd/203c43b5e02e15a7d955a237a231b32b to your computer and use it in GitHub Desktop.
A faster way to display ImageMagick/Wand images in Windows x64 without creating temporary files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| __all__ = ["display"] | |
| """ | |
| A faster way to display ImageMagick/Wand images in Windows, | |
| without creating temporary files. | |
| Usage: | |
| ====== | |
| from wand.image import Image | |
| import sys | |
| if sys.platform == "win32": | |
| from wand_win_display import display | |
| else: | |
| from wand.display import display | |
| with Image(filename="lena.png") as img: | |
| display(img) | |
| """ | |
| from ctypes import * | |
| from ctypes.wintypes import * | |
| import io | |
| # Config | |
| MIN_WIN_SIZE = 240 | |
| ######################################## | |
| # Used Winapi structs | |
| ######################################## | |
| # https://learn.microsoft.com/sr-latn-rs/windows/win32/api/winuser/ns-winuser-paintstruct | |
| class PAINTSTRUCT(Structure): | |
| _fields_ = [ | |
| ("hdc", HDC), | |
| ("fErase", BOOL), | |
| ("rcPaint", RECT), | |
| ("fRestore", BOOL), | |
| ("fIncUpdate", BOOL), | |
| ("rgbReserved", BYTE * 32), | |
| ] | |
| LONG_PTR = c_longlong # for x64 only! | |
| WNDPROC = WINFUNCTYPE(LONG_PTR, HWND, UINT, WPARAM, LPARAM) | |
| # https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw | |
| class WNDCLASSEXW(Structure): | |
| def __init__(self, *args, **kwargs): | |
| super(WNDCLASSEXW, self).__init__(*args, **kwargs) | |
| self.cbSize = sizeof(self) | |
| _fields_ = [ | |
| ("cbSize", UINT), | |
| ("style", UINT), | |
| ("lpfnWndProc", WNDPROC), | |
| ("cbClsExtra", INT), | |
| ("cbWndExtra", INT), | |
| ("hInstance", HANDLE), | |
| ("hIcon", HANDLE), | |
| ("hCursor", HANDLE), | |
| ("hBrush", HANDLE), | |
| ("lpszMenuName", LPCWSTR), | |
| ("lpszClassName", LPCWSTR), | |
| ("hIconSm", HANDLE) | |
| ] | |
| # https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader | |
| class BITMAPINFOHEADER(Structure): | |
| def __init__(self, *args, **kwargs): | |
| super(BITMAPINFOHEADER, self).__init__(*args, **kwargs) | |
| self.biSize = sizeof(self) | |
| _fields_ = [ | |
| ("biSize", DWORD), | |
| ("biWidth", LONG), | |
| ("biHeight", LONG), | |
| ("biPlanes", WORD), | |
| ("biBitCount", WORD), | |
| ("biCompression", DWORD), | |
| ("biSizeImage", DWORD), | |
| ("biXPelsPerMeter", LONG), | |
| ("biYPelsPerMeter", LONG), | |
| ("biClrUsed", DWORD), | |
| ("biClrImportant", DWORD) | |
| ] | |
| LPBITMAPINFOHEADER = POINTER(BITMAPINFOHEADER) | |
| # https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-minmaxinfo | |
| class MINMAXINFO(Structure): | |
| _fields_ = [ | |
| ("ptReserved", POINT), | |
| ("ptMaxSize", POINT), | |
| ("ptMaxPosition", POINT), | |
| ("ptMinTrackSize", POINT), | |
| ("ptMaxTrackSize", POINT), | |
| ] | |
| ######################################## | |
| # Used Winapi functions | |
| ######################################## | |
| gdi32 = windll.Gdi32 | |
| gdi32.CreateCompatibleDC.argtypes = (HDC,) | |
| gdi32.CreateCompatibleDC.restype = HDC | |
| gdi32.CreateDIBSection.argtypes = (HDC, LPVOID, UINT, LPVOID, HANDLE, DWORD) | |
| gdi32.CreateDIBSection.restype = HBITMAP | |
| gdi32.DeleteDC.argtypes = (HDC,) | |
| gdi32.DeleteObject.argtypes = (HANDLE,) | |
| gdi32.GetStockObject.restype = HANDLE | |
| gdi32.SelectObject.argtypes = (HDC, HANDLE) | |
| gdi32.SelectObject.restype = HANDLE | |
| gdi32.SetDIBits.argtypes = (HDC, HBITMAP, UINT, UINT, LPVOID, LPVOID, UINT) | |
| gdi32.SetStretchBltMode.argtypes = (HDC, INT) | |
| gdi32.StretchBlt.argtypes = (HDC, INT, INT, INT, INT, HDC, INT, INT, INT, INT, DWORD) | |
| kernel32 = windll.Kernel32 | |
| kernel32.GetModuleHandleW.argtypes = (LPCWSTR,) | |
| kernel32.GetModuleHandleW.restype = HINSTANCE | |
| user32 = windll.user32 | |
| user32.BeginPaint.argtypes = (HWND, POINTER(PAINTSTRUCT)) | |
| user32.CreateWindowExW.argtypes = (DWORD, LPCWSTR, LPCWSTR, DWORD, INT, INT, INT, INT, HWND, HMENU, HINSTANCE, LPVOID) | |
| user32.DefWindowProcW.argtypes = (HWND, UINT, WPARAM, LPARAM) | |
| user32.DestroyWindow.argtypes = (HWND,) | |
| user32.DispatchMessageW.argtypes = (POINTER(MSG),) | |
| user32.EndPaint.argtypes = (HWND, POINTER(PAINTSTRUCT)) | |
| user32.GetMessageW.argtypes = (POINTER(MSG),HWND,UINT,UINT) | |
| user32.LoadCursorW.argtypes = (HINSTANCE, LPVOID) | |
| user32.LoadCursorW.restype = HANDLE | |
| user32.LoadIconW.argtypes = (HINSTANCE, LPCWSTR) | |
| user32.LoadIconW.restype = HICON | |
| user32.PostMessageW.argtypes = (HWND, UINT, LPVOID, LPVOID) | |
| user32.SystemParametersInfoA.argtypes = (UINT, UINT, LPVOID, UINT) | |
| user32.TranslateMessage.argtypes = (POINTER(MSG),) | |
| ######################################## | |
| # Used Winapi constants | |
| ######################################## | |
| BI_RGB = 0 | |
| BLACK_BRUSH = 4 | |
| CS_HREDRAW = 2 | |
| CS_VREDRAW = 1 | |
| DIB_RGB_COLORS = 0 | |
| HALFTONE = 4 | |
| IDC_ARROW = 32512 | |
| SM_CYCAPTION = 4 | |
| SPI_GETWORKAREA = 48 | |
| SRCCOPY = 13369376 | |
| WM_CLOSE = 16 | |
| WM_GETMINMAXINFO = 36 | |
| WM_PAINT = 15 | |
| WM_QUIT = 18 | |
| WS_OVERLAPPEDWINDOW = 13565952 | |
| WS_VISIBLE = 268435456 | |
| def display(img, title = None): | |
| """ Displays ImageMagick/Wand image in a native window, without creating any temporary file. """ | |
| def _image_to_hbitmap(img): | |
| """ Convert ImageMagick image to HBITMAP """ | |
| img = img.clone() | |
| img.format = "dib" | |
| img.type = "truecolor" # Force RGB | |
| f = io.BytesIO() | |
| img.save(file=f) | |
| data = f.getvalue() | |
| bmih = cast(data, LPBITMAPINFOHEADER) | |
| h_bitmap = gdi32.CreateDIBSection(None, bmih, DIB_RGB_COLORS, None, None, 0) | |
| gdi32.SetDIBits( | |
| None, h_bitmap, | |
| 0, img.height, | |
| data[sizeof(BITMAPINFOHEADER):], # Skip the BITMAPINFOHEADER (40 bytes) | |
| bmih, | |
| DIB_RGB_COLORS | |
| ) | |
| return h_bitmap | |
| def _get_win_rect_for_image(im): | |
| """ | |
| Show window centered on screen and never bigger than | |
| the actual work area (desktop minus taskbar) | |
| """ | |
| rc_desktop = RECT() | |
| user32.SystemParametersInfoA(SPI_GETWORKAREA, 0, byref(rc_desktop), 0) | |
| rc_desktop.right -= 32 # Windows 11 DWM fix | |
| caption_height = user32.GetSystemMetrics(SM_CYCAPTION) | |
| win_width, win_height = im.width, im.height + caption_height | |
| if win_width > rc_desktop.right or win_height > rc_desktop.bottom: | |
| desktop_ratio = rc_desktop.right / (rc_desktop.bottom - caption_height) | |
| if desktop_ratio > im_ratio: | |
| win_height = rc_desktop.bottom | |
| win_width = round(win_height * desktop_ratio) | |
| else: | |
| win_width = rc_desktop.right | |
| win_height = round(win_width / desktop_ratio) | |
| x = (rc_desktop.right - win_width) // 2 + 16 | |
| y = (rc_desktop.bottom - win_height) // 2 | |
| return x, y, win_width, win_height | |
| h_bitmap = _image_to_hbitmap(img) | |
| img_ratio = img.width / img.height | |
| def _on_WM_PAINT(hwnd, wparam, lparam): | |
| """ Shows image centered and resized to window while keeping its aspect ratio. """ | |
| rc = RECT() | |
| user32.GetClientRect(hwnd, byref(rc)) | |
| width, height = rc.right, rc.bottom | |
| if width / height > img_ratio: | |
| dest_width = round(height * img_ratio) | |
| dest_height = height | |
| x = (width - dest_width) // 2 | |
| y = 0 | |
| else: | |
| dest_width = width | |
| dest_height = round(width / img_ratio) | |
| x = 0 | |
| y = (height - dest_height) // 2 | |
| ps = PAINTSTRUCT() | |
| hdc = user32.BeginPaint(hwnd, byref(ps)) | |
| gdi32.SetStretchBltMode(hdc, HALFTONE) | |
| hdc_mem = gdi32.CreateCompatibleDC(hdc) | |
| gdi32.SelectObject(hdc_mem, h_bitmap) | |
| gdi32.StretchBlt( | |
| hdc, x, y, dest_width, dest_height, # dest | |
| hdc_mem, 0, 0, img.width, img.height, # scr | |
| SRCCOPY | |
| ) | |
| gdi32.DeleteDC(hdc_mem) | |
| user32.EndPaint(hwnd, byref(ps)) | |
| return 0 | |
| def _window_proc_callback(hwnd, msg, wparam, lparam): | |
| if msg == WM_CLOSE: | |
| user32.PostMessageW(hwnd, WM_QUIT, 0, 0) | |
| elif msg == WM_GETMINMAXINFO: | |
| mmi = cast(lparam, POINTER(MINMAXINFO)) | |
| mmi.contents.ptMinTrackSize = POINT(MIN_WIN_SIZE, MIN_WIN_SIZE) | |
| return 0 | |
| elif msg == WM_PAINT: | |
| return _on_WM_PAINT(hwnd, wparam, lparam) | |
| return user32.DefWindowProcW(hwnd, msg, wparam, lparam) | |
| wndclass = WNDCLASSEXW() | |
| wndclass.lpfnWndProc = WNDPROC(_window_proc_callback) | |
| wndclass.style = CS_VREDRAW | CS_HREDRAW | |
| wndclass.lpszClassName = "WandWinDisplay" | |
| wndclass.hBrush = gdi32.GetStockObject(BLACK_BRUSH) | |
| wndclass.hCursor = user32.LoadCursorW(0, IDC_ARROW) | |
| wndclass.hIcon = user32.LoadIconW(kernel32.GetModuleHandleW(None), LPCWSTR(1)) # Python icon | |
| wndclass.hIconSm = wndclass.hIcon | |
| user32.RegisterClassExW(byref(wndclass)) | |
| hwnd = user32.CreateWindowExW( | |
| 0, | |
| wndclass.lpszClassName, | |
| title or f"{img.format} - {img.type} - {img.width} x {img.height}", | |
| WS_OVERLAPPEDWINDOW | WS_VISIBLE, | |
| *_get_win_rect_for_image(img), | |
| None, None, None, None | |
| ) | |
| msg = MSG() | |
| while user32.GetMessageW(byref(msg), 0, 0, 0) > 0: | |
| user32.TranslateMessage(byref(msg)) | |
| user32.DispatchMessageW(byref(msg)) | |
| user32.DestroyWindow(hwnd) | |
| gdi32.DeleteObject(h_bitmap) | |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage in Windows 11
