Skip to content

Instantly share code, notes, and snippets.

@59de44955ebd
Last active September 21, 2025 09:59
Show Gist options
  • Save 59de44955ebd/1614d742337d5a3a639029287dac49b6 to your computer and use it in GitHub Desktop.
Save 59de44955ebd/1614d742337d5a3a639029287dac49b6 to your computer and use it in GitHub Desktop.
A better Image.show() implementation for PIL on Windows
__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