Last active
September 21, 2025 09:59
-
-
Save 59de44955ebd/1614d742337d5a3a639029287dac49b6 to your computer and use it in GitHub Desktop.
A better Image.show() implementation for PIL on Windows
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__ = [] | |
""" | |
Importing this script in Python 3.x x64 on Windows overwrites | |
PIL's default (slow) 'Image.show()' implementation based on | |
temporary PNGs with a faster implementation that instead shows | |
the image in a native resizable viewer window, without creating | |
any temporary files. In addition to PIL the script only uses | |
ctypes and the Windows API, no 3rd-party modules involved. | |
Usage: | |
====== | |
from PIL import Image | |
import pil_show_windows # import overwrites show() method | |
img = Image.open('D:\\test\\lena.png') | |
img.show() # Blocks code execution until viewer window is closed | |
... | |
""" | |
from ctypes import Structure, sizeof, POINTER, WINFUNCTYPE, windll, c_longlong, byref | |
from ctypes.wintypes import * | |
from PIL import ImageShow, ImageWin | |
######################################## | |
# Used Winapi structs | |
######################################## | |
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) | |
class WNDCLASSEX(Structure): | |
def __init__(self, *args, **kwargs): | |
super(WNDCLASSEX, 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) | |
] | |
######################################## | |
# Used Winapi functions | |
######################################## | |
gdi32 = windll.Gdi32 | |
gdi32.GetStockObject.restype = HANDLE | |
gdi32.SetStretchBltMode.argtypes = (HDC, INT) | |
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 | |
######################################## | |
BLACK_BRUSH = 4 | |
CS_HREDRAW = 2 | |
CS_VREDRAW = 1 | |
CW_USEDEFAULT = -2147483648 | |
HALFTONE = 4 | |
IDC_ARROW = 32512 | |
SM_CYCAPTION = 4 | |
SPI_GETWORKAREA = 48 | |
WM_CLOSE = 16 | |
WM_PAINT = 15 | |
WM_QUIT = 18 | |
WS_OVERLAPPEDWINDOW = 13565952 | |
WS_VISIBLE = 268435456 | |
class PILImageShow(): | |
def __init__(self, img, window_title='PIL Image'): | |
if img.mode not in ('RGB', 'RGBA', 'L', '1'): | |
img = img.convert('RGB') | |
dib = ImageWin.Dib(img) | |
img_ratio = img.width / img.height | |
# Show image centered and resized to window while keeping its aspect ratio | |
def _on_WM_PAINT(hwnd, wparam, lparam): | |
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) | |
dib.draw(ImageWin.HDC(hdc), (x, y, x + dest_width, y + dest_height)) | |
user32.EndPaint(hwnd, byref(ps)) | |
return 0 | |
def _window_proc_callback(hwnd, msg, wparam, lparam): | |
if msg == WM_PAINT: | |
return _on_WM_PAINT(hwnd, wparam, lparam) | |
elif msg == WM_CLOSE: | |
user32.PostMessageW(self.hwnd, WM_QUIT, 0, 0) | |
return user32.DefWindowProcW(hwnd, msg, wparam, lparam) | |
wndclass = WNDCLASSEX() | |
wndclass.lpfnWndProc = WNDPROC(_window_proc_callback) | |
wndclass.style = CS_VREDRAW | CS_HREDRAW | |
wndclass.lpszClassName = 'PILImageShow' | |
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)) | |
# 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 = img.width, img.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 > img_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 | |
self.hwnd = user32.CreateWindowExW( | |
0, | |
wndclass.lpszClassName, | |
window_title, | |
WS_OVERLAPPEDWINDOW | WS_VISIBLE, | |
x, y, | |
win_width, win_height, | |
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(self.hwnd) | |
# This overwrites PIL Image's show() method with our custom implementation | |
setattr(ImageShow, 'show', lambda img, title=None, **kwargs: | |
PILImageShow(img, title or (img.filename if hasattr(img, 'filename') else 'PIL Image')) | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment