From 37c9b523b218cb0683ab4226a6fd855b04bac969 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Apr 2021 19:54:01 +1000 Subject: [PATCH 1/4] Use constant for uncompressed RGB data check --- src/PIL/DdsImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 8af87b53f..a2163605c 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -130,8 +130,8 @@ class DdsImageFile(ImageFile.ImageFile): fourcc = header.read(4) (bitcount,) = struct.unpack(" Date: Mon, 17 May 2021 20:48:05 +1000 Subject: [PATCH 2/4] Added RGB saving --- Tests/test_file_dds.py | 18 +++++++++++++++++- src/PIL/DdsImagePlugin.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index f74556567..b44afff3a 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -5,7 +5,7 @@ import pytest from PIL import DdsImagePlugin, Image -from .helper import assert_image_equal, assert_image_equal_tofile +from .helper import assert_image_equal, assert_image_equal_tofile, hopper TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds" @@ -242,3 +242,19 @@ def test_unimplemented_pixel_format(): with pytest.raises(NotImplementedError): with Image.open("Tests/images/unimplemented_pixel_format.dds"): pass + + +def test_save_unsupported_mode(tmp_path): + out = str(tmp_path / "temp.dds") + im = hopper("HSV") + with pytest.raises(OSError): + im.save(out) + + +def test_save(tmp_path): + out = str(tmp_path / "temp.dds") + im = hopper() + im.save(out) + + with Image.open(out) as reloaded: + assert_image_equal(im, reloaded) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index a2163605c..e8a41c045 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -14,6 +14,7 @@ import struct from io import BytesIO from . import Image, ImageFile +from ._binary import o32le as o32 # Magic ("DDS ") DDS_MAGIC = 0x20534444 @@ -201,9 +202,43 @@ class DdsImageFile(ImageFile.ImageFile): pass +def _save(im, fp, filename): + if im.mode != "RGB": + raise OSError(f"cannot write mode {im.mode} as DDS") + + fp.write( + o32(DDS_MAGIC) + + o32(124) # header size + + o32( + DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PITCH | DDSD_PIXELFORMAT + ) # flags + + o32(im.height) + + o32(im.width) + + o32((im.width * 24 + 7) // 8) # pitch + + o32(0) # depth + + o32(0) # mipmaps + + o32(0) * 11 # reserved + + o32(32) # pfsize + + o32(DDPF_RGB) # pfflags + + o32(0) # fourcc + + o32(24) # bitcount + + o32(0xFF0000) # rbitmask + + o32(0xFF00) # gbitmask + + o32(0xFF) # bbitmask + + o32(0) # abitmask + + o32(DDSCAPS_TEXTURE) # dwCaps + + o32(0) # dwCaps2 + + o32(0) # dwCaps3 + + o32(0) # dwCaps4 + + o32(0) # dwReserved2 + ) + ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (im.mode[::-1], 0, 1))]) + + def _accept(prefix): return prefix[:4] == b"DDS " Image.register_open(DdsImageFile.format, DdsImageFile, _accept) +Image.register_save(DdsImageFile.format, _save) Image.register_extension(DdsImageFile.format, ".dds") From 6449cdc1a3d3c8ddc436aca766198d149add486a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 17 May 2021 20:48:31 +1000 Subject: [PATCH 3/4] Added RGBA saving --- Tests/test_file_dds.py | 18 +++++++++++++----- src/PIL/DdsImagePlugin.py | 13 ++++++++----- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index b44afff3a..46ebcad0c 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -251,10 +251,18 @@ def test_save_unsupported_mode(tmp_path): im.save(out) -def test_save(tmp_path): +@pytest.mark.parametrize( + ("mode", "test_file"), + [ + ("RGB", "Tests/images/hopper.png"), + ("RGBA", "Tests/images/pil123rgba.png"), + ], +) +def test_save(mode, test_file, tmp_path): out = str(tmp_path / "temp.dds") - im = hopper() - im.save(out) + with Image.open(test_file) as im: + assert im.mode == mode + im.save(out) - with Image.open(out) as reloaded: - assert_image_equal(im, reloaded) + with Image.open(out) as reloaded: + assert_image_equal(im, reloaded) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index e8a41c045..260924fca 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -203,7 +203,7 @@ class DdsImageFile(ImageFile.ImageFile): def _save(im, fp, filename): - if im.mode != "RGB": + if im.mode not in ("RGB", "RGBA"): raise OSError(f"cannot write mode {im.mode} as DDS") fp.write( @@ -214,24 +214,27 @@ def _save(im, fp, filename): ) # flags + o32(im.height) + o32(im.width) - + o32((im.width * 24 + 7) // 8) # pitch + + o32((im.width * (32 if im.mode == "RGBA" else 24) + 7) // 8) # pitch + o32(0) # depth + o32(0) # mipmaps + o32(0) * 11 # reserved + o32(32) # pfsize - + o32(DDPF_RGB) # pfflags + + o32(DDS_RGBA if im.mode == "RGBA" else DDPF_RGB) # pfflags + o32(0) # fourcc - + o32(24) # bitcount + + o32(32 if im.mode == "RGBA" else 24) # bitcount + o32(0xFF0000) # rbitmask + o32(0xFF00) # gbitmask + o32(0xFF) # bbitmask - + o32(0) # abitmask + + o32(0xFF000000 if im.mode == "RGBA" else 0) # abitmask + o32(DDSCAPS_TEXTURE) # dwCaps + o32(0) # dwCaps2 + o32(0) # dwCaps3 + o32(0) # dwCaps4 + o32(0) # dwReserved2 ) + if im.mode == "RGBA": + r, g, b, a = im.split() + im = Image.merge("RGBA", (a, r, g, b)) ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (im.mode[::-1], 0, 1))]) From 2afc6fdfc4f5fc5595e1dda5baa8625d81112fe2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 17 May 2021 20:52:03 +1000 Subject: [PATCH 4/4] Documented updates to DDS format --- docs/handbook/image-file-formats.rst | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index a0949914a..358ae5281 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -39,6 +39,13 @@ The :py:meth:`~PIL.Image.open` method sets the following **compression** Set to ``bmp_rle`` if the file is run-length encoded. +DDS +^^^ + +DDS is a popular container texture format used in video games and natively supported +by DirectX. Uncompressed RGB and RGBA can be read, and (since 8.3.0) written. DXT1, +DXT3 (since 3.4.0) and DXT5 pixel formats can be read, only in ``RGBA`` mode. + DIB ^^^ @@ -1042,17 +1049,6 @@ is commonly used in fax applications. The DCX decoder can read files containing When the file is opened, only the first image is read. You can use :py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images. - -DDS -^^^ - -DDS is a popular container texture format used in video games and natively -supported by DirectX. -Currently, uncompressed RGB data and DXT1, DXT3, and DXT5 pixel formats are -supported, and only in ``RGBA`` mode. - -.. versionadded:: 3.4.0 DXT3 - FLI, FLC ^^^^^^^^