Lazy import only required plugin

This commit is contained in:
Hugo van Kemenade 2026-01-18 22:59:28 +02:00
parent e2b87a0420
commit 6b9de40533

View File

@ -323,6 +323,99 @@ def getmodebands(mode: str) -> int:
_initialized = 0
# Mapping from file extension to plugin module name for lazy loading
_EXTENSION_PLUGIN: dict[str, str] = {
# Common formats (preinit)
".bmp": "BmpImagePlugin",
".dib": "BmpImagePlugin",
".gif": "GifImagePlugin",
".jfif": "JpegImagePlugin",
".jpe": "JpegImagePlugin",
".jpg": "JpegImagePlugin",
".jpeg": "JpegImagePlugin",
".pbm": "PpmImagePlugin",
".pgm": "PpmImagePlugin",
".pnm": "PpmImagePlugin",
".ppm": "PpmImagePlugin",
".pfm": "PpmImagePlugin",
".png": "PngImagePlugin",
".apng": "PngImagePlugin",
# Less common formats (init)
".avif": "AvifImagePlugin",
".avifs": "AvifImagePlugin",
".blp": "BlpImagePlugin",
".bufr": "BufrStubImagePlugin",
".cur": "CurImagePlugin",
".dcx": "DcxImagePlugin",
".dds": "DdsImagePlugin",
".ps": "EpsImagePlugin",
".eps": "EpsImagePlugin",
".fit": "FitsImagePlugin",
".fits": "FitsImagePlugin",
".fli": "FliImagePlugin",
".flc": "FliImagePlugin",
".fpx": "FpxImagePlugin",
".ftc": "FtexImagePlugin",
".ftu": "FtexImagePlugin",
".gbr": "GbrImagePlugin",
".grib": "GribStubImagePlugin",
".h5": "Hdf5StubImagePlugin",
".hdf": "Hdf5StubImagePlugin",
".icns": "IcnsImagePlugin",
".ico": "IcoImagePlugin",
".im": "ImImagePlugin",
".iim": "IptcImagePlugin",
".jp2": "Jpeg2KImagePlugin",
".j2k": "Jpeg2KImagePlugin",
".jpc": "Jpeg2KImagePlugin",
".jpf": "Jpeg2KImagePlugin",
".jpx": "Jpeg2KImagePlugin",
".j2c": "Jpeg2KImagePlugin",
".mic": "MicImagePlugin",
".mpg": "MpegImagePlugin",
".mpeg": "MpegImagePlugin",
".mpo": "MpoImagePlugin",
".msp": "MspImagePlugin",
".palm": "PalmImagePlugin",
".pcd": "PcdImagePlugin",
".pcx": "PcxImagePlugin",
".pdf": "PdfImagePlugin",
".pxr": "PixarImagePlugin",
".psd": "PsdImagePlugin",
".qoi": "QoiImagePlugin",
".bw": "SgiImagePlugin",
".rgb": "SgiImagePlugin",
".rgba": "SgiImagePlugin",
".sgi": "SgiImagePlugin",
".ras": "SunImagePlugin",
".tga": "TgaImagePlugin",
".icb": "TgaImagePlugin",
".vda": "TgaImagePlugin",
".vst": "TgaImagePlugin",
".tif": "TiffImagePlugin",
".tiff": "TiffImagePlugin",
".webp": "WebPImagePlugin",
".wmf": "WmfImagePlugin",
".emf": "WmfImagePlugin",
".xbm": "XbmImagePlugin",
".xpm": "XpmImagePlugin",
}
def _load_plugin_for_extension(ext: str | bytes) -> bool:
"""Load only the plugin needed for a specific file extension."""
if isinstance(ext, bytes):
ext = ext.decode()
plugin = _EXTENSION_PLUGIN.get(ext.lower())
if plugin is None:
return False
try:
__import__(f"PIL.{plugin}", globals(), locals(), [])
return True
except ImportError:
return False
def preinit() -> None:
"""
@ -2535,11 +2628,13 @@ class Image:
# only set the name for metadata purposes
filename = os.fspath(fp.name)
preinit()
filename_ext = os.path.splitext(filename)[1].lower()
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
# Try loading only the plugin for this extension first
if not _load_plugin_for_extension(ext):
preinit()
if not format:
if ext not in EXTENSION:
init()
@ -3524,7 +3619,11 @@ def open(
prefix = fp.read(16)
preinit()
# Try to load just the plugin needed for this file extension
# before falling back to preinit() which loads common plugins
ext = os.path.splitext(filename)[1] if filename else ""
if not (ext and _load_plugin_for_extension(ext)):
preinit()
warning_messages: list[str] = []
@ -3560,14 +3659,19 @@ def open(
im = _open_core(fp, filename, prefix, formats)
if im is None and formats is ID:
checked_formats = ID.copy()
if init():
im = _open_core(
fp,
filename,
prefix,
tuple(format for format in formats if format not in checked_formats),
)
# Try preinit (few common plugins) then init (all plugins)
for loader in (preinit, init):
checked_formats = ID.copy()
loader()
if formats != checked_formats:
im = _open_core(
fp,
filename,
prefix,
tuple(f for f in formats if f not in checked_formats),
)
if im is not None:
break
if im:
im._exclusive_fp = exclusive_fp