Skip to content

Commit 6b9de40

Browse files
committed
Lazy import only required plugin
1 parent e2b87a0 commit 6b9de40

File tree

1 file changed

+115
-11
lines changed

1 file changed

+115
-11
lines changed

src/PIL/Image.py

Lines changed: 115 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,99 @@ def getmodebands(mode: str) -> int:
323323

324324
_initialized = 0
325325

326+
# Mapping from file extension to plugin module name for lazy loading
327+
_EXTENSION_PLUGIN: dict[str, str] = {
328+
# Common formats (preinit)
329+
".bmp": "BmpImagePlugin",
330+
".dib": "BmpImagePlugin",
331+
".gif": "GifImagePlugin",
332+
".jfif": "JpegImagePlugin",
333+
".jpe": "JpegImagePlugin",
334+
".jpg": "JpegImagePlugin",
335+
".jpeg": "JpegImagePlugin",
336+
".pbm": "PpmImagePlugin",
337+
".pgm": "PpmImagePlugin",
338+
".pnm": "PpmImagePlugin",
339+
".ppm": "PpmImagePlugin",
340+
".pfm": "PpmImagePlugin",
341+
".png": "PngImagePlugin",
342+
".apng": "PngImagePlugin",
343+
# Less common formats (init)
344+
".avif": "AvifImagePlugin",
345+
".avifs": "AvifImagePlugin",
346+
".blp": "BlpImagePlugin",
347+
".bufr": "BufrStubImagePlugin",
348+
".cur": "CurImagePlugin",
349+
".dcx": "DcxImagePlugin",
350+
".dds": "DdsImagePlugin",
351+
".ps": "EpsImagePlugin",
352+
".eps": "EpsImagePlugin",
353+
".fit": "FitsImagePlugin",
354+
".fits": "FitsImagePlugin",
355+
".fli": "FliImagePlugin",
356+
".flc": "FliImagePlugin",
357+
".fpx": "FpxImagePlugin",
358+
".ftc": "FtexImagePlugin",
359+
".ftu": "FtexImagePlugin",
360+
".gbr": "GbrImagePlugin",
361+
".grib": "GribStubImagePlugin",
362+
".h5": "Hdf5StubImagePlugin",
363+
".hdf": "Hdf5StubImagePlugin",
364+
".icns": "IcnsImagePlugin",
365+
".ico": "IcoImagePlugin",
366+
".im": "ImImagePlugin",
367+
".iim": "IptcImagePlugin",
368+
".jp2": "Jpeg2KImagePlugin",
369+
".j2k": "Jpeg2KImagePlugin",
370+
".jpc": "Jpeg2KImagePlugin",
371+
".jpf": "Jpeg2KImagePlugin",
372+
".jpx": "Jpeg2KImagePlugin",
373+
".j2c": "Jpeg2KImagePlugin",
374+
".mic": "MicImagePlugin",
375+
".mpg": "MpegImagePlugin",
376+
".mpeg": "MpegImagePlugin",
377+
".mpo": "MpoImagePlugin",
378+
".msp": "MspImagePlugin",
379+
".palm": "PalmImagePlugin",
380+
".pcd": "PcdImagePlugin",
381+
".pcx": "PcxImagePlugin",
382+
".pdf": "PdfImagePlugin",
383+
".pxr": "PixarImagePlugin",
384+
".psd": "PsdImagePlugin",
385+
".qoi": "QoiImagePlugin",
386+
".bw": "SgiImagePlugin",
387+
".rgb": "SgiImagePlugin",
388+
".rgba": "SgiImagePlugin",
389+
".sgi": "SgiImagePlugin",
390+
".ras": "SunImagePlugin",
391+
".tga": "TgaImagePlugin",
392+
".icb": "TgaImagePlugin",
393+
".vda": "TgaImagePlugin",
394+
".vst": "TgaImagePlugin",
395+
".tif": "TiffImagePlugin",
396+
".tiff": "TiffImagePlugin",
397+
".webp": "WebPImagePlugin",
398+
".wmf": "WmfImagePlugin",
399+
".emf": "WmfImagePlugin",
400+
".xbm": "XbmImagePlugin",
401+
".xpm": "XpmImagePlugin",
402+
}
403+
404+
405+
def _load_plugin_for_extension(ext: str | bytes) -> bool:
406+
"""Load only the plugin needed for a specific file extension."""
407+
if isinstance(ext, bytes):
408+
ext = ext.decode()
409+
plugin = _EXTENSION_PLUGIN.get(ext.lower())
410+
if plugin is None:
411+
return False
412+
413+
try:
414+
__import__(f"PIL.{plugin}", globals(), locals(), [])
415+
return True
416+
except ImportError:
417+
return False
418+
326419

327420
def preinit() -> None:
328421
"""
@@ -2535,11 +2628,13 @@ def save(
25352628
# only set the name for metadata purposes
25362629
filename = os.fspath(fp.name)
25372630

2538-
preinit()
2539-
25402631
filename_ext = os.path.splitext(filename)[1].lower()
25412632
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
25422633

2634+
# Try loading only the plugin for this extension first
2635+
if not _load_plugin_for_extension(ext):
2636+
preinit()
2637+
25432638
if not format:
25442639
if ext not in EXTENSION:
25452640
init()
@@ -3524,7 +3619,11 @@ def open(
35243619

35253620
prefix = fp.read(16)
35263621

3527-
preinit()
3622+
# Try to load just the plugin needed for this file extension
3623+
# before falling back to preinit() which loads common plugins
3624+
ext = os.path.splitext(filename)[1] if filename else ""
3625+
if not (ext and _load_plugin_for_extension(ext)):
3626+
preinit()
35283627

35293628
warning_messages: list[str] = []
35303629

@@ -3560,14 +3659,19 @@ def _open_core(
35603659
im = _open_core(fp, filename, prefix, formats)
35613660

35623661
if im is None and formats is ID:
3563-
checked_formats = ID.copy()
3564-
if init():
3565-
im = _open_core(
3566-
fp,
3567-
filename,
3568-
prefix,
3569-
tuple(format for format in formats if format not in checked_formats),
3570-
)
3662+
# Try preinit (few common plugins) then init (all plugins)
3663+
for loader in (preinit, init):
3664+
checked_formats = ID.copy()
3665+
loader()
3666+
if formats != checked_formats:
3667+
im = _open_core(
3668+
fp,
3669+
filename,
3670+
prefix,
3671+
tuple(f for f in formats if f not in checked_formats),
3672+
)
3673+
if im is not None:
3674+
break
35713675

35723676
if im:
35733677
im._exclusive_fp = exclusive_fp

0 commit comments

Comments
 (0)