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 reading DDS BGR;15 images #7574

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
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
38 changes: 30 additions & 8 deletions src/PIL/DdsImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,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 +147,38 @@ 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]
else:
if not pfflags & DDPF_ALPHAPIXELS:
self._mode = "RGB"
rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF]

self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))]
# Each channel is derived using a mask of the pixel values
masks = {
mask: "RGB"[i]
for i, mask in enumerate(struct.unpack("<3I", header.read(12)))
Copy link
Contributor

Choose a reason for hiding this comment

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

The previous version always read 16 bytes for masks – this will now read either 12 or 16 bytes. That seems odd?

Copy link
Member Author

Choose a reason for hiding this comment

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

It isn't always necessary to read the last 4 bytes, and therefore seems more efficient to not do so where possible.

If you are concerned about the file pointer position, that's not relevant, as it is just reading from

header = BytesIO(header_bytes)

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would actually be more efficient and clearer to not do another read-and-struct-unpack? (Would also mean that the entire DDS_PIXELFORMAT is always consumed.)

}
if self.mode == "RGB" and all(
mask in (0b0000000000011111, 0b0000001111100000, 0b0111110000000000)
Copy link
Contributor

Choose a reason for hiding this comment

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

This did become less clear to read, you're right. Wonder if these and the 0xFF0000 masks from below could benefit from becoming constants á la (I didn't double check whether I have RGB/BGR right here):

R_MASK_555 = 0b0000000000011111
G_MASK_555 = 0b000001111100000
B_MASK_555 = 0b111110000000000
R_MASK_888 = 0x000000FF
G_MASK_888 = 0x0000FF00
B_MASK_888 = 0x00FF0000
A_MASK_888 = 0xFF000000

for mask in masks
):
# Each mask uses 5 bits
rawmode = (
masks[0b0000000000011111]
+ masks[0b0000001111100000]
+ masks[0b0111110000000000]
+ ";15"
)
Comment on lines +163 to +168
Copy link
Contributor

@akx akx Nov 29, 2023

Choose a reason for hiding this comment

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

Will this now support esoteric formats like GRB;15 (and if so, will everything be handled correctly down the line)? If not, then I think just "RGB;15" would be better?

EDIT: right, the whole point here was BGR;15 – my bad, too little coffee.

Copy link
Member Author

Choose a reason for hiding this comment

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

This will support RGB;15 and BGR;15

Pillow/src/libImaging/Unpack.c

Lines 1576 to 1577 in 697c24b

{"RGB", "RGB;15", 16, ImagingUnpackRGB15},
{"RGB", "BGR;15", 16, ImagingUnpackBGR15},

else:
# Try 8 bits for each mask
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the trying here? Rather, what happens if trying fails?

Copy link
Member Author

Choose a reason for hiding this comment

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

Given that the issue related to this issue has found 5 bit masks, it seems possible that there could be other mask values used in files, beyond 5 bit and 8 bits.

If it fails, an error is raised, and ultimately an UnidentifiedImageError.

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
# Try 8 bits for each mask
# Assume 8 bits for each mask

if self.mode == "RGBA":
# The fourth mask, alpha,
# is only valid when the correct flags are present
Copy link
Contributor

Choose a reason for hiding this comment

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

What are the correct flags?

Copy link
Member Author

Choose a reason for hiding this comment

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

https://learn.microsoft.com/en-us/windows/win32/direct3ddds/dds-pixelformat seems inconsistent on this to me

DDPF_ALPHAPIXELS | Texture contains alpha data; dwRGBAlphaBitMask contains valid data.

Alpha mask for reading alpha data. dwFlags must include DDPF_ALPHAPIXELS or DDPF_ALPHA

The code is checking for DDPF_ALPHAPIXELS

if not pfflags & DDPF_ALPHAPIXELS:

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
# is only valid when the correct flags are present
# is only valid when `pfflags & DDPF_ALPHAPIXELS`

a_mask = struct.unpack("<I", header.read(4))[0]
masks[a_mask] = "A"

rawmode = masks[0xFF] + masks[0xFF00] + masks[0xFF0000]
if 0xFF000000 in masks:
rawmode += masks[0xFF000000]

self.tile = [("raw", (0, 0) + self.size, 0, (rawmode, 0, 1))]
elif pfflags & DDPF_PALETTEINDEXED8:
self._mode = "P"
self.palette = ImagePalette.raw("RGBA", self.fp.read(1024))
Expand Down