Skip to content

Commit

Permalink
Merge pull request #7589 from radarhere/dds_rgb
Browse files Browse the repository at this point in the history
  • Loading branch information
hugovk committed Dec 31, 2023
2 parents 5df7235 + 232094e commit 119885a
Show file tree
Hide file tree
Showing 6 changed files with 48 additions and 24 deletions.
Binary file added Tests/images/bgr15.dds
Binary file not shown.
Binary file added Tests/images/bgr15.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes.
Binary file removed Tests/images/unsupported_bitcount_rgb.dds
Binary file not shown.
13 changes: 4 additions & 9 deletions Tests/test_file_dds.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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 @@ -249,6 +250,7 @@ def test_dx10_r8g8b8a8_unorm_srgb():
("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 Expand Up @@ -341,16 +343,9 @@ def test_palette():
assert_image_equal_tofile(im, "Tests/images/transparent.gif")


@pytest.mark.parametrize(
"test_file",
(
"Tests/images/unsupported_bitcount_rgb.dds",
"Tests/images/unsupported_bitcount_luminance.dds",
),
)
def test_unsupported_bitcount(test_file):
def test_unsupported_bitcount():
with pytest.raises(OSError):
with Image.open(test_file):
with Image.open("Tests/images/unsupported_bitcount.dds"):
pass


Expand Down
59 changes: 44 additions & 15 deletions src/PIL/DdsImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

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

# Magic ("DDS ")
Expand Down Expand Up @@ -341,6 +342,7 @@ def _open(self):

flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height)
extents = (0, 0) + self.size

pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved
Expand All @@ -351,22 +353,16 @@ def _open(self):
rawmode = None
if pfflags & DDPF.RGB:
# Texture contains uncompressed RGB data
masks = struct.unpack("<4I", header.read(16))
masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
if bitcount == 24:
self._mode = "RGB"
rawmode = masks[0x000000FF] + masks[0x0000FF00] + masks[0x00FF0000]
elif bitcount == 32 and pfflags & DDPF.ALPHAPIXELS:
if pfflags & DDPF.ALPHAPIXELS:
self._mode = "RGBA"
rawmode = (
masks[0x000000FF]
+ masks[0x0000FF00]
+ masks[0x00FF0000]
+ masks[0xFF000000]
)
mask_count = 4
else:
msg = f"Unsupported bitcount {bitcount} for {pfflags}"
raise OSError(msg)
self._mode = "RGB"
mask_count = 3

masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4))
self.tile = [("dds_rgb", extents, 0, (bitcount, masks))]
return
elif pfflags & DDPF.LUMINANCE:
if bitcount == 8:
self._mode = "L"
Expand Down Expand Up @@ -464,7 +460,6 @@ def _open(self):
msg = f"Unknown pixel format flags {pfflags}"
raise NotImplementedError(msg)

extents = (0, 0) + self.size
if n:
self.tile = [
ImageFile._Tile("bcn", extents, offset, (n, self.pixel_format))
Expand All @@ -476,6 +471,39 @@ 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):
value = int.from_bytes(self.fd.read(bytecount), "little")
for i, mask in enumerate(masks):
masked_value = 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 @@ -533,5 +561,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")

0 comments on commit 119885a

Please sign in to comment.