Skip to content

Commit d0394d4

Browse files
authored
Merge pull request #5402 from radarhere/dds
2 parents b5c4b9a + 2afc6fd commit d0394d4

File tree

3 files changed

+72
-14
lines changed

3 files changed

+72
-14
lines changed

Tests/test_file_dds.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from PIL import DdsImagePlugin, Image
77

8-
from .helper import assert_image_equal, assert_image_equal_tofile
8+
from .helper import assert_image_equal, assert_image_equal_tofile, hopper
99

1010
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
1111
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
@@ -242,3 +242,27 @@ def test_unimplemented_pixel_format():
242242
with pytest.raises(NotImplementedError):
243243
with Image.open("Tests/images/unimplemented_pixel_format.dds"):
244244
pass
245+
246+
247+
def test_save_unsupported_mode(tmp_path):
248+
out = str(tmp_path / "temp.dds")
249+
im = hopper("HSV")
250+
with pytest.raises(OSError):
251+
im.save(out)
252+
253+
254+
@pytest.mark.parametrize(
255+
("mode", "test_file"),
256+
[
257+
("RGB", "Tests/images/hopper.png"),
258+
("RGBA", "Tests/images/pil123rgba.png"),
259+
],
260+
)
261+
def test_save(mode, test_file, tmp_path):
262+
out = str(tmp_path / "temp.dds")
263+
with Image.open(test_file) as im:
264+
assert im.mode == mode
265+
im.save(out)
266+
267+
with Image.open(out) as reloaded:
268+
assert_image_equal(im, reloaded)

docs/handbook/image-file-formats.rst

+7-11
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ The :py:meth:`~PIL.Image.open` method sets the following
3939
**compression**
4040
Set to ``bmp_rle`` if the file is run-length encoded.
4141

42+
DDS
43+
^^^
44+
45+
DDS is a popular container texture format used in video games and natively supported
46+
by DirectX. Uncompressed RGB and RGBA can be read, and (since 8.3.0) written. DXT1,
47+
DXT3 (since 3.4.0) and DXT5 pixel formats can be read, only in ``RGBA`` mode.
48+
4249
DIB
4350
^^^
4451

@@ -1042,17 +1049,6 @@ is commonly used in fax applications. The DCX decoder can read files containing
10421049
When the file is opened, only the first image is read. You can use
10431050
:py:meth:`~PIL.Image.Image.seek` or :py:mod:`~PIL.ImageSequence` to read other images.
10441051

1045-
1046-
DDS
1047-
^^^
1048-
1049-
DDS is a popular container texture format used in video games and natively
1050-
supported by DirectX.
1051-
Currently, uncompressed RGB data and DXT1, DXT3, and DXT5 pixel formats are
1052-
supported, and only in ``RGBA`` mode.
1053-
1054-
.. versionadded:: 3.4.0 DXT3
1055-
10561052
FLI, FLC
10571053
^^^^^^^^
10581054

src/PIL/DdsImagePlugin.py

+40-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from io import BytesIO
1515

1616
from . import Image, ImageFile
17+
from ._binary import o32le as o32
1718

1819
# Magic ("DDS ")
1920
DDS_MAGIC = 0x20534444
@@ -130,8 +131,8 @@ def _open(self):
130131
fourcc = header.read(4)
131132
(bitcount,) = struct.unpack("<I", header.read(4))
132133
masks = struct.unpack("<4I", header.read(16))
133-
if pfflags & 0x40:
134-
# DDPF_RGB - Texture contains uncompressed RGB data
134+
if pfflags & DDPF_RGB:
135+
# Texture contains uncompressed RGB data
135136
masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
136137
rawmode = ""
137138
if bitcount == 32:
@@ -201,9 +202,46 @@ def load_seek(self, pos):
201202
pass
202203

203204

205+
def _save(im, fp, filename):
206+
if im.mode not in ("RGB", "RGBA"):
207+
raise OSError(f"cannot write mode {im.mode} as DDS")
208+
209+
fp.write(
210+
o32(DDS_MAGIC)
211+
+ o32(124) # header size
212+
+ o32(
213+
DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PITCH | DDSD_PIXELFORMAT
214+
) # flags
215+
+ o32(im.height)
216+
+ o32(im.width)
217+
+ o32((im.width * (32 if im.mode == "RGBA" else 24) + 7) // 8) # pitch
218+
+ o32(0) # depth
219+
+ o32(0) # mipmaps
220+
+ o32(0) * 11 # reserved
221+
+ o32(32) # pfsize
222+
+ o32(DDS_RGBA if im.mode == "RGBA" else DDPF_RGB) # pfflags
223+
+ o32(0) # fourcc
224+
+ o32(32 if im.mode == "RGBA" else 24) # bitcount
225+
+ o32(0xFF0000) # rbitmask
226+
+ o32(0xFF00) # gbitmask
227+
+ o32(0xFF) # bbitmask
228+
+ o32(0xFF000000 if im.mode == "RGBA" else 0) # abitmask
229+
+ o32(DDSCAPS_TEXTURE) # dwCaps
230+
+ o32(0) # dwCaps2
231+
+ o32(0) # dwCaps3
232+
+ o32(0) # dwCaps4
233+
+ o32(0) # dwReserved2
234+
)
235+
if im.mode == "RGBA":
236+
r, g, b, a = im.split()
237+
im = Image.merge("RGBA", (a, r, g, b))
238+
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (im.mode[::-1], 0, 1))])
239+
240+
204241
def _accept(prefix):
205242
return prefix[:4] == b"DDS "
206243

207244

208245
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
246+
Image.register_save(DdsImageFile.format, _save)
209247
Image.register_extension(DdsImageFile.format, ".dds")

0 commit comments

Comments
 (0)