Repository: python-pillow/Pillow

Issue:
Add a specific exception if image not recognized?
Hello,

if the file provided as parameter to `Image.open()` is not recognized, an `IOError` is raised. It's nice but the exception is not really precise and, in python3.3+, it equals to `OSError` so it can be surprising depending on the used traceback.

The raised exception could be specific like `UnknownImageError`. It should inherit from `IOError` for backward compatibility.

The main modification would be in Image.py, line 2730:
```python
    for message in accept_warnings:
        warnings.warn(message)
-    raise IOError("cannot identify image file %r"
+    raise UnknownImageError("cannot identify image file %r"
                 % (filename if filename else fp))
```

What do you think about it? I will send a PR if you agree with the idea.



Discussion:
Related to, but not the same as, #1643 and #2339
Agreed, this is definitely not an OSError. It has nothing to do with the underlying operating system.

Catching this exception is awkward because one must do a string comparison against `str(e)`. (Pillow does not set `strerror` properly, but it would still not be nice.)

```python
except OSError as e:
    if 'cannot identify image file' in str(e):
        # do whatever in the case the format is not recognized
    else:
        raise
```
I think it needs custom exception classes to check what's the problem like above, Tracebacks are too poor. 
Pillow is almost used as standard library.

Resolving patch:
diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py
--- a/src/PIL/GdImageFile.py
+++ b/src/PIL/GdImageFile.py
@@ -23,7 +23,7 @@
 # purposes only.
 
 
-from . import ImageFile, ImagePalette
+from . import ImageFile, ImagePalette, UnidentifiedImageError
 from ._binary import i8, i16be as i16, i32be as i32
 
 ##
@@ -82,4 +82,4 @@ def open(fp, mode="r"):
     try:
         return GdImageFile(fp)
     except SyntaxError:
-        raise IOError("cannot identify this image file")
+        raise UnidentifiedImageError("cannot identify this image file")
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -38,7 +38,7 @@
 # VERSION was removed in Pillow 6.0.0.
 # PILLOW_VERSION was removed in Pillow 7.0.0.
 # Use __version__ instead.
-from . import ImageMode, TiffTags, __version__, _plugins
+from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
 from ._binary import i8, i32le
 from ._util import deferred_error, isPath, isStringType, py3
 
@@ -2794,7 +2794,9 @@ def _open_core(fp, filename, prefix):
         fp.close()
     for message in accept_warnings:
         warnings.warn(message)
-    raise IOError("cannot identify image file %r" % (filename if filename else fp))
+    raise UnidentifiedImageError(
+        "cannot identify image file %r" % (filename if filename else fp)
+    )
 
 
 #
diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py
--- a/src/PIL/__init__.py
+++ b/src/PIL/__init__.py
@@ -70,3 +70,7 @@
     "XpmImagePlugin",
     "XVThumbImagePlugin",
 ]
+
+
+class UnidentifiedImageError(IOError):
+    pass


Test patch:
diff --git a/Tests/test_file_gd.py b/Tests/test_file_gd.py
--- a/Tests/test_file_gd.py
+++ b/Tests/test_file_gd.py
@@ -1,4 +1,4 @@
-from PIL import GdImageFile
+from PIL import GdImageFile, UnidentifiedImageError
 
 from .helper import PillowTestCase
 
@@ -17,4 +17,4 @@ def test_bad_mode(self):
     def test_invalid_file(self):
         invalid_file = "Tests/images/flower.jpg"
 
-        self.assertRaises(IOError, GdImageFile.open, invalid_file)
+        self.assertRaises(UnidentifiedImageError, GdImageFile.open, invalid_file)
diff --git a/Tests/test_image.py b/Tests/test_image.py
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -2,7 +2,7 @@
 import shutil
 import tempfile
 
-from PIL import Image
+from PIL import Image, UnidentifiedImageError
 from PIL._util import py3
 
 from .helper import PillowTestCase, hopper, is_win32, unittest
@@ -48,6 +48,9 @@ def test_image_modes_fail(self):
                 Image.new(mode, (1, 1))
             self.assertEqual(str(e.exception), "unrecognized image mode")
 
+    def test_exception_inheritance(self):
+        self.assertTrue(issubclass(UnidentifiedImageError, IOError))
+
     def test_sanity(self):
 
         im = Image.new("L", (100, 100))
@@ -88,7 +91,7 @@ def test_invalid_image(self):
             import StringIO
 
             im = StringIO.StringIO("")
-        self.assertRaises(IOError, Image.open, im)
+        self.assertRaises(UnidentifiedImageError, Image.open, im)
 
     def test_bad_mode(self):
         self.assertRaises(ValueError, Image.open, "filename", "bad mode")