Repository: python-pillow/Pillow

Issue:
DDS Plugin cannot handle uncompressed files
Loading a DDS file without compression gives me the following error:
```
  File "<SNIP>\PIL\Image.py", line 2676, in open
    im = _open_core(fp, filename, prefix)
  File "<SNIP>\PIL\Image.py", line 2658, in _open_core
    im = factory(fp, filename)
  File "<SNIP>\PIL\ImageFile.py", line 103, in __init__
    self._open()
  File "<SNIP>\PIL\DdsImagePlugin.py", line 158, in _open
    (fourcc))
NotImplementedError: Unimplemented pixel format b'\x00\x00\x00\x00'
```

To demonstrate the issue, I created a simple DDS file as demonstration. Calling `Image.open()` on that file will not work. 
[uncompressed_dds.zip](https://github.com/python-pillow/Pillow/files/2872238/uncompressed_dds.zip)

Looking over the source code of DdsImagePlugin, it seems that only FourCC DDS files are supported.

Pillow version: 5.4.1
Python version: 3.7.2


Discussion:
The docs say only DXT1, DXT3 and DXT5 in `RGBA`:

> DDS is a popular container texture format used in video games and natively supported by DirectX. Currently, DXT1, DXT3, and DXT5 pixel formats are supported and only in `RGBA` mode.

https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html?highlight=dds#dds

The code also includes three DXGI formats of DXT10:

https://github.com/python-pillow/Pillow/blob/daa8b5133923561d2c6d4d1f7b2c9d9aa233007e/src/PIL/DdsImagePlugin.py#L131-L158
Ah, I hadn't seen the documentation. Let's turn this into a feature request, then.
@Xfel is the file that you have provided able to added to the test suite under the Pillow license?
I created this file specifically for this demonstration, so it should be fine. (it's just a bunch of random scribbles anyways). 

Resolving patch:
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -123,43 +123,52 @@ def _open(self):
         # pixel format
         pfsize, pfflags = struct.unpack("<2I", header.read(8))
         fourcc = header.read(4)
-        bitcount, rmask, gmask, bmask, amask = struct.unpack("<5I",
-                                                             header.read(20))
-
-        data_start = header_size + 4
-        n = 0
-        if fourcc == b"DXT1":
-            self.pixel_format = "DXT1"
-            n = 1
-        elif fourcc == b"DXT3":
-            self.pixel_format = "DXT3"
-            n = 2
-        elif fourcc == b"DXT5":
-            self.pixel_format = "DXT5"
-            n = 3
-        elif fourcc == b"DX10":
-            data_start += 20
-            # ignoring flags which pertain to volume textures and cubemaps
-            dxt10 = BytesIO(self.fp.read(20))
-            dxgi_format, dimension = struct.unpack("<II", dxt10.read(8))
-            if dxgi_format in (DXGI_FORMAT_BC7_TYPELESS,
-                               DXGI_FORMAT_BC7_UNORM):
-                self.pixel_format = "BC7"
-                n = 7
-            elif dxgi_format == DXGI_FORMAT_BC7_UNORM_SRGB:
-                self.pixel_format = "BC7"
-                self.im_info["gamma"] = 1/2.2
-                n = 7
-            else:
-                raise NotImplementedError("Unimplemented DXGI format %d" %
-                                          (dxgi_format))
+        bitcount, = struct.unpack("<I", header.read(4))
+        masks = struct.unpack("<4I", header.read(16))
+        if pfflags & 0x40:
+            # DDPF_RGB - Texture contains uncompressed RGB data
+            masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
+            rawmode = ""
+            if bitcount == 32:
+                rawmode += masks[0xff000000]
+            rawmode += masks[0xff0000] + masks[0xff00] + masks[0xff]
+
+            self.tile = [("raw", (0, 0) + self.size, 0, (rawmode, 0, 1))]
         else:
-            raise NotImplementedError("Unimplemented pixel format %r" %
-                                      (fourcc))
+            data_start = header_size + 4
+            n = 0
+            if fourcc == b"DXT1":
+                self.pixel_format = "DXT1"
+                n = 1
+            elif fourcc == b"DXT3":
+                self.pixel_format = "DXT3"
+                n = 2
+            elif fourcc == b"DXT5":
+                self.pixel_format = "DXT5"
+                n = 3
+            elif fourcc == b"DX10":
+                data_start += 20
+                # ignoring flags which pertain to volume textures and cubemaps
+                dxt10 = BytesIO(self.fp.read(20))
+                dxgi_format, dimension = struct.unpack("<II", dxt10.read(8))
+                if dxgi_format in (DXGI_FORMAT_BC7_TYPELESS,
+                                   DXGI_FORMAT_BC7_UNORM):
+                    self.pixel_format = "BC7"
+                    n = 7
+                elif dxgi_format == DXGI_FORMAT_BC7_UNORM_SRGB:
+                    self.pixel_format = "BC7"
+                    self.im_info["gamma"] = 1/2.2
+                    n = 7
+                else:
+                    raise NotImplementedError("Unimplemented DXGI format %d" %
+                                              (dxgi_format))
+            else:
+                raise NotImplementedError("Unimplemented pixel format %r" %
+                                          (fourcc))
 
-        self.tile = [
-            ("bcn", (0, 0) + self.size, data_start, (n))
-        ]
+            self.tile = [
+                ("bcn", (0, 0) + self.size, data_start, (n))
+            ]
 
     def load_seek(self, pos):
         pass


Test patch:
diff --git a/Tests/images/uncompressed_rgb.dds b/Tests/images/uncompressed_rgb.dds
new file mode 100755
Binary files /dev/null and b/Tests/images/uncompressed_rgb.dds differ
diff --git a/Tests/images/uncompressed_rgb.png b/Tests/images/uncompressed_rgb.png
new file mode 100644
Binary files /dev/null and b/Tests/images/uncompressed_rgb.png differ
diff --git a/Tests/images/unimplemented_dxgi_format.dds b/Tests/images/unimplemented_dxgi_format.dds
new file mode 100644
Binary files /dev/null and b/Tests/images/unimplemented_dxgi_format.dds differ
diff --git a/Tests/images/unimplemented_pixel_format.dds b/Tests/images/unimplemented_pixel_format.dds
new file mode 100755
Binary files /dev/null and b/Tests/images/unimplemented_pixel_format.dds differ
diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py
--- a/Tests/test_file_dds.py
+++ b/Tests/test_file_dds.py
@@ -7,6 +7,7 @@
 TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
 TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
 TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
+TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/uncompressed_rgb.dds"
 
 
 class TestFileDds(PillowTestCase):
@@ -67,6 +68,24 @@ def test_dx10_bc7(self):
 
         self.assert_image_equal(target, im)
 
+    def test_unimplemented_dxgi_format(self):
+        self.assertRaises(NotImplementedError, Image.open,
+                          "Tests/images/unimplemented_dxgi_format.dds")
+
+    def test_uncompressed_rgb(self):
+        """Check uncompressed RGB images can be opened"""
+
+        target = Image.open(TEST_FILE_UNCOMPRESSED_RGB.replace('.dds', '.png'))
+
+        im = Image.open(TEST_FILE_UNCOMPRESSED_RGB)
+        im.load()
+
+        self.assertEqual(im.format, "DDS")
+        self.assertEqual(im.mode, "RGBA")
+        self.assertEqual(im.size, (800, 600))
+
+        self.assert_image_equal(target, im)
+
     def test__validate_true(self):
         """Check valid prefix"""
         # Arrange
@@ -110,3 +129,7 @@ def short_file():
             im.load()
 
         self.assertRaises(IOError, short_file)
+
+    def test_unimplemented_pixel_format(self):
+        self.assertRaises(NotImplementedError, Image.open,
+                          "Tests/images/unimplemented_pixel_format.dds")