Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added DDS support for uncompressed L and LA images #6820

Merged
merged 4 commits into from
Dec 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Tests/images/uncompressed_l.dds
Binary file not shown.
Binary file added Tests/images/uncompressed_l.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/uncompressed_la.dds
Binary file not shown.
Binary file added Tests/images/uncompressed_la.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 19 additions & 17 deletions Tests/test_file_dds.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
TEST_FILE_DX10_BC7_UNORM_SRGB = "Tests/images/DXGI_FORMAT_BC7_UNORM_SRGB.dds"
TEST_FILE_DX10_R8G8B8A8 = "Tests/images/argb-32bpp_MipMaps-1.dds"
TEST_FILE_DX10_R8G8B8A8_UNORM_SRGB = "Tests/images/DXGI_FORMAT_R8G8B8A8_UNORM_SRGB.dds"
TEST_FILE_UNCOMPRESSED_L = "Tests/images/uncompressed_l.dds"
TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA = "Tests/images/uncompressed_la.dds"
TEST_FILE_UNCOMPRESSED_RGB = "Tests/images/hopper.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"

Expand Down Expand Up @@ -194,26 +196,24 @@ def test_unimplemented_dxgi_format():
pass


def test_uncompressed_rgb():
"""Check uncompressed RGB images can be opened"""

# convert -format dds -define dds:compression=none hopper.jpg hopper.dds
with Image.open(TEST_FILE_UNCOMPRESSED_RGB) as im:
assert im.format == "DDS"
assert im.mode == "RGB"
assert im.size == (128, 128)

assert_image_equal_tofile(im, "Tests/images/hopper.png")
@pytest.mark.parametrize(
("mode", "size", "test_file"),
[
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
],
)
def test_uncompressed(mode, size, test_file):
"""Check uncompressed images can be opened"""

# Test image with alpha
with Image.open(TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA) as im:
with Image.open(test_file) as im:
assert im.format == "DDS"
assert im.mode == "RGBA"
assert im.size == (800, 600)
assert im.mode == mode
assert im.size == size

assert_image_equal_tofile(
im, TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA.replace(".dds", ".png")
)
assert_image_equal_tofile(im, test_file.replace(".dds", ".png"))


def test__accept_true():
Expand Down Expand Up @@ -305,6 +305,8 @@ def test_save_unsupported_mode(tmp_path):
@pytest.mark.parametrize(
("mode", "test_file"),
[
("L", "Tests/images/linear_gradient.png"),
("LA", "Tests/images/uncompressed_la.png"),
("RGB", "Tests/images/hopper.png"),
("RGBA", "Tests/images/pil123rgba.png"),
],
Expand Down
7 changes: 4 additions & 3 deletions docs/releasenotes/9.4.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ TODO
Other Changes
=============

TODO
^^^^
Added support for DDS L and LA images
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

TODO
Support has been added to read and write L and LA DDS images in the uncompressed
format, known as "luminance" textures.
42 changes: 31 additions & 11 deletions src/PIL/DdsImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,19 @@ def _open(self):
fourcc = header.read(4)
(bitcount,) = struct.unpack("<I", header.read(4))
masks = struct.unpack("<4I", header.read(16))
if pfflags & DDPF_RGB:
if pfflags & DDPF_LUMINANCE:
# Texture contains uncompressed L or LA data
if pfflags & DDPF_ALPHAPIXELS:
self.mode = "LA"
else:
self.mode = "L"

self.tile = [("raw", (0, 0) + self.size, 0, (self.mode, 0, 1))]
elif pfflags & DDPF_RGB:
# Texture contains uncompressed RGB data
masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
rawmode = ""
if bitcount == 32:
if pfflags & DDPF_ALPHAPIXELS:
rawmode += masks[0xFF000000]
else:
self.mode = "RGB"
Expand Down Expand Up @@ -223,9 +231,24 @@ def load_seek(self, pos):


def _save(im, fp, filename):
if im.mode not in ("RGB", "RGBA"):
if im.mode not in ("RGB", "RGBA", "L", "LA"):
raise OSError(f"cannot write mode {im.mode} as DDS")

rawmode = im.mode
masks = [0xFF0000, 0xFF00, 0xFF]
if im.mode in ("L", "LA"):
pixel_flags = DDPF_LUMINANCE
else:
pixel_flags = DDPF_RGB
rawmode = rawmode[::-1]
if im.mode in ("LA", "RGBA"):
pixel_flags |= DDPF_ALPHAPIXELS
masks.append(0xFF000000)

bitcount = len(masks) * 8
while len(masks) < 4:
masks.append(0)

fp.write(
o32(DDS_MAGIC)
+ o32(124) # header size
Expand All @@ -234,18 +257,15 @@ def _save(im, fp, filename):
) # flags
+ o32(im.height)
+ o32(im.width)
+ o32((im.width * (32 if im.mode == "RGBA" else 24) + 7) // 8) # pitch
+ o32((im.width * bitcount + 7) // 8) # pitch
+ o32(0) # depth
+ o32(0) # mipmaps
+ o32(0) * 11 # reserved
+ o32(32) # pfsize
+ o32(DDS_RGBA if im.mode == "RGBA" else DDPF_RGB) # pfflags
+ o32(pixel_flags) # pfflags
+ o32(0) # fourcc
+ o32(32 if im.mode == "RGBA" else 24) # bitcount
+ o32(0xFF0000) # rbitmask
+ o32(0xFF00) # gbitmask
+ o32(0xFF) # bbitmask
+ o32(0xFF000000 if im.mode == "RGBA" else 0) # abitmask
+ o32(bitcount) # bitcount
+ b"".join(o32(mask) for mask in masks) # rgbabitmask
+ o32(DDSCAPS_TEXTURE) # dwCaps
+ o32(0) # dwCaps2
+ o32(0) # dwCaps3
Expand All @@ -255,7 +275,7 @@ def _save(im, fp, filename):
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))])
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])


def _accept(prefix):
Expand Down