@@ -323,6 +323,108 @@ def getmodebands(mode: str) -> int:
323323
324324_initialized = 0
325325
326+ # Mapping from file extension to plugin module name for lazy importing
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 _import_plugin_for_extension (ext : str | bytes ) -> bool :
406+ """Import only the plugin needed for a specific file extension."""
407+ if not ext :
408+ return False
409+
410+ if isinstance (ext , bytes ):
411+ ext = ext .decode ()
412+ ext = ext .lower ()
413+ if ext in EXTENSION :
414+ return True
415+
416+ plugin = _EXTENSION_PLUGIN .get (ext )
417+ if plugin is None :
418+ return False
419+
420+ try :
421+ logger .debug ("Importing %s" , plugin )
422+ __import__ (f"{ __spec__ .parent } .{ plugin } " , globals (), locals (), [])
423+ return True
424+ except ImportError as e :
425+ logger .debug ("Image: failed to import %s: %s" , plugin , e )
426+ return False
427+
326428
327429def preinit () -> None :
328430 """
@@ -382,11 +484,10 @@ def init() -> bool:
382484 if _initialized >= 2 :
383485 return False
384486
385- parent_name = __name__ .rpartition ("." )[0 ]
386487 for plugin in _plugins :
387488 try :
388489 logger .debug ("Importing %s" , plugin )
389- __import__ (f"{ parent_name } .{ plugin } " , globals (), locals (), [])
490+ __import__ (f"{ __spec__ . parent } .{ plugin } " , globals (), locals (), [])
390491 except ImportError as e :
391492 logger .debug ("Image: failed to import %s: %s" , plugin , e )
392493
@@ -2535,12 +2636,20 @@ def save(
25352636 # only set the name for metadata purposes
25362637 filename = os .fspath (fp .name )
25372638
2538- preinit ()
2639+ if format :
2640+ preinit ()
2641+ else :
2642+ filename_ext = os .path .splitext (filename )[1 ].lower ()
2643+ ext = (
2644+ filename_ext .decode ()
2645+ if isinstance (filename_ext , bytes )
2646+ else filename_ext
2647+ )
25392648
2540- filename_ext = os .path .splitext (filename )[1 ].lower ()
2541- ext = filename_ext .decode () if isinstance (filename_ext , bytes ) else filename_ext
2649+ # Try importing only the plugin for this extension first
2650+ if not _import_plugin_for_extension (ext ):
2651+ preinit ()
25422652
2543- if not format :
25442653 if ext not in EXTENSION :
25452654 init ()
25462655 try :
@@ -3524,7 +3633,11 @@ def open(
35243633
35253634 prefix = fp .read (16 )
35263635
3527- preinit ()
3636+ # Try to import just the plugin needed for this file extension
3637+ # before falling back to preinit() which imports common plugins
3638+ ext = os .path .splitext (filename )[1 ] if filename else ""
3639+ if not _import_plugin_for_extension (ext ):
3640+ preinit ()
35283641
35293642 warning_messages : list [str ] = []
35303643
@@ -3560,14 +3673,19 @@ def _open_core(
35603673 im = _open_core (fp , filename , prefix , formats )
35613674
35623675 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- )
3676+ # Try preinit (few common plugins) then init (all plugins)
3677+ for loader in (preinit , init ):
3678+ checked_formats = ID .copy ()
3679+ loader ()
3680+ if formats != checked_formats :
3681+ im = _open_core (
3682+ fp ,
3683+ filename ,
3684+ prefix ,
3685+ tuple (f for f in formats if f not in checked_formats ),
3686+ )
3687+ if im is not None :
3688+ break
35713689
35723690 if im :
35733691 im ._exclusive_fp = exclusive_fp
0 commit comments