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

Support arbitrary masks for uncompressed RGB DDS images #7589

Merged
merged 5 commits into from Dec 31, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added Tests/images/bgr15.dds
Binary file not shown.
Binary file added Tests/images/bgr15.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions Tests/test_file_dds.py
Expand Up @@ -26,6 +26,7 @@
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_BGR15 = "Tests/images/bgr15.dds"
TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds"


Expand Down Expand Up @@ -211,6 +212,7 @@ def test_unimplemented_dxgi_format():
("L", (128, 128), TEST_FILE_UNCOMPRESSED_L),
("LA", (128, 128), TEST_FILE_UNCOMPRESSED_L_WITH_ALPHA),
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_RGB),
("RGB", (128, 128), TEST_FILE_UNCOMPRESSED_BGR15),
("RGBA", (800, 600), TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA),
],
)
Expand Down
48 changes: 42 additions & 6 deletions src/PIL/DdsImagePlugin.py
Expand Up @@ -14,6 +14,7 @@
from io import BytesIO

from . import Image, ImageFile, ImagePalette
from ._binary import o8
from ._binary import o32le as o32

# Magic ("DDS ")
Expand Down Expand Up @@ -137,7 +138,6 @@ def _open(self):
pfsize, pfflags = struct.unpack("<2I", header.read(8))
fourcc = header.read(4)
(bitcount,) = struct.unpack("<I", header.read(4))
masks = struct.unpack("<4I", header.read(16))
if pfflags & DDPF_LUMINANCE:
# Texture contains uncompressed L or LA data
if pfflags & DDPF_ALPHAPIXELS:
Expand All @@ -148,15 +148,16 @@ def _open(self):
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 pfflags & DDPF_ALPHAPIXELS:
rawmode += masks[0xFF000000]
mask_count = 4
else:
self._mode = "RGB"
rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF]
mask_count = 3

self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))]
masks = struct.unpack(
"<" + str(mask_count) + "I", header.read(mask_count * 4)
radarhere marked this conversation as resolved.
Show resolved Hide resolved
)
self.tile = [("dds_rgb", (0, 0) + self.size, 0, (bitcount, masks))]
elif pfflags & DDPF_PALETTEINDEXED8:
self._mode = "P"
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
Expand Down Expand Up @@ -237,6 +238,40 @@ def load_seek(self, pos):
pass


class DdsRgbDecoder(ImageFile.PyDecoder):
_pulls_fd = True

def decode(self, buffer):
bitcount, masks = self.args

# Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
# Calculate how many zeros each mask is padded with
mask_offsets = []
# And the maximum value of each channel without the padding
mask_totals = []
for mask in masks:
offset = 0
if mask != 0:
while mask >> (offset + 1) << (offset + 1) == mask:
offset += 1
mask_offsets.append(offset)
mask_totals.append(mask >> offset)

data = bytearray()
bytecount = bitcount // 8
while len(data) < self.state.xsize * self.state.ysize * len(masks):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth it to precalculate the total here to avoid that extra multiplication every loop (since we know the value can't possibly change):

Suggested change
while len(data) < self.state.xsize * self.state.ysize * len(masks):
total_size = self.state.xsize * self.state.ysize * len(masks)
while len(data) < total_size:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally think the current version is simpler to read, and expect the performance difference to be negligible.

The current version is also used in a few other places in Pillow - but at the same time, that's probably because I wrote them.

while len(data) < self.state.xsize * self.state.ysize * bands:

value = self.fd.read(bytecount)
int_value = sum(value[i] << i * 8 for i in range(bytecount))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int.from_bytes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I've pushed a commit.

for i, mask in enumerate(masks):
masked_value = int_value & mask
# Remove the zero padding, and scale it to 8 bits
data += o8(
int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255)
)
self.set_as_raw(bytes(data))
return -1, 0


def _save(im, fp, filename):
if im.mode not in ("RGB", "RGBA", "L", "LA"):
msg = f"cannot write mode {im.mode} as DDS"
Expand Down Expand Up @@ -291,5 +326,6 @@ def _accept(prefix):


Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
Image.register_decoder("dds_rgb", DdsRgbDecoder)
Image.register_save(DdsImageFile.format, _save)
Image.register_extension(DdsImageFile.format, ".dds")