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 BC5 reading #5501

Merged
merged 7 commits into from Jun 11, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added Tests/images/bc5_snorm.dds
Binary file not shown.
Binary file added Tests/images/bc5_typeless.dds
Binary file not shown.
Binary file added Tests/images/bc5_unorm.dds
Binary file not shown.
Binary file added Tests/images/bc5_unorm.png
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/bc5s.dds
Binary file not shown.
Binary file added Tests/images/bc5s.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 34 additions & 6 deletions Tests/test_file_dds.py
Expand Up @@ -10,6 +10,10 @@
TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds"
TEST_FILE_DXT3 = "Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds"
TEST_FILE_DXT5 = "Tests/images/dxt5-argb-8bbp-interpolatedalpha_MipMaps-1.dds"
TEST_FILE_DX10_BC5_TYPELESS = "Tests/images/bc5_typeless.dds"
TEST_FILE_DX10_BC5_UNORM = "Tests/images/bc5_unorm.dds"
TEST_FILE_DX10_BC5_SNORM = "Tests/images/bc5_snorm.dds"
TEST_FILE_BC5S = "Tests/images/bc5s.dds"
TEST_FILE_DX10_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"
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"
Expand All @@ -32,6 +36,19 @@ def test_sanity_dxt1():
assert_image_equal(im, target)


def test_sanity_dxt3():
"""Check DXT3 images can be opened"""

with Image.open(TEST_FILE_DXT3) as im:
im.load()

assert im.format == "DDS"
assert im.mode == "RGBA"
assert im.size == (256, 256)

assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png"))


def test_sanity_dxt5():
"""Check DXT5 images can be opened"""

Expand All @@ -45,17 +62,28 @@ def test_sanity_dxt5():
assert_image_equal_tofile(im, TEST_FILE_DXT5.replace(".dds", ".png"))


def test_sanity_dxt3():
"""Check DXT3 images can be opened"""

with Image.open(TEST_FILE_DXT3) as im:
@pytest.mark.parametrize(
("image_path", "expected_path"),
(
# hexeditted to be typeless
(TEST_FILE_DX10_BC5_TYPELESS, TEST_FILE_DX10_BC5_UNORM),
(TEST_FILE_DX10_BC5_UNORM, TEST_FILE_DX10_BC5_UNORM),
# hexeditted to use DX10 FourCC
(TEST_FILE_DX10_BC5_SNORM, TEST_FILE_BC5S),
(TEST_FILE_BC5S, TEST_FILE_BC5S),
),
)
def test_dx10_bc5(image_path, expected_path):
"""Check DX10 BC5 images can be opened"""

with Image.open(image_path) as im:
im.load()

assert im.format == "DDS"
assert im.mode == "RGBA"
assert im.mode == "RGB"
assert im.size == (256, 256)

assert_image_equal_tofile(im, TEST_FILE_DXT3.replace(".dds", ".png"))
assert_image_equal_tofile(im, expected_path.replace(".dds", ".png"))


def test_dx10_bc7():
Expand Down
7 changes: 4 additions & 3 deletions docs/releasenotes/8.3.0.rst
Expand Up @@ -58,7 +58,8 @@ TODO
Other Changes
=============

TODO
^^^^
Added DDS BC5 reading
^^^^^^^^^^^^^^^^^^^^^

TODO
Support has been added to read the BC5 format of DDS images, whether UNORM, SNORM or
TYPELESS.
25 changes: 21 additions & 4 deletions src/PIL/DdsImagePlugin.py
Expand Up @@ -97,6 +97,9 @@
DXGI_FORMAT_R8G8B8A8_TYPELESS = 27
DXGI_FORMAT_R8G8B8A8_UNORM = 28
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29
DXGI_FORMAT_BC5_TYPELESS = 82
DXGI_FORMAT_BC5_UNORM = 83
DXGI_FORMAT_BC5_SNORM = 84
DXGI_FORMAT_BC7_TYPELESS = 97
DXGI_FORMAT_BC7_UNORM = 98
DXGI_FORMAT_BC7_UNORM_SRGB = 99
Expand Down Expand Up @@ -150,12 +153,24 @@ def _open(self):
elif fourcc == b"DXT5":
self.pixel_format = "DXT5"
n = 3
elif fourcc == b"BC5S":
self.pixel_format = "BC5S"
n = 5
self.mode = "RGB"
elif fourcc == b"DX10":
data_start += 20
# ignoring flags which pertain to volume textures and cubemaps
dxt10 = BytesIO(self.fp.read(20))
dxgi_format, dimension = struct.unpack("<II", dxt10.read(8))
if dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM):
(dxgi_format,) = struct.unpack("<I", self.fp.read(4))
self.fp.read(16)
if dxgi_format in (DXGI_FORMAT_BC5_TYPELESS, DXGI_FORMAT_BC5_UNORM):
self.pixel_format = "BC5"
n = 5
self.mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC5_SNORM:
self.pixel_format = "BC5S"
n = 5
self.mode = "RGB"
elif dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM):
self.pixel_format = "BC7"
n = 7
elif dxgi_format == DXGI_FORMAT_BC7_UNORM_SRGB:
Expand All @@ -178,7 +193,9 @@ def _open(self):
else:
raise NotImplementedError(f"Unimplemented pixel format {repr(fourcc)}")

self.tile = [("bcn", (0, 0) + self.size, data_start, (n))]
self.tile = [
("bcn", (0, 0) + self.size, data_start, (n, self.pixel_format))
]

def load_seek(self, pos):
pass
Expand Down
15 changes: 9 additions & 6 deletions src/decode.c
Expand Up @@ -34,9 +34,10 @@

#include "libImaging/Imaging.h"

#include "libImaging/Bit.h"
#include "libImaging/Bcn.h"
#include "libImaging/Gif.h"
#include "libImaging/Raw.h"
#include "libImaging/Bit.h"
#include "libImaging/Sgi.h"

/* -------------------------------------------------------------------- */
Expand Down Expand Up @@ -359,22 +360,24 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) {
char *mode;
char *actual;
int n = 0;
int ystep = 1;
if (!PyArg_ParseTuple(args, "s|ii", &mode, &n, &ystep)) {
char *pixel_format = "";
if (!PyArg_ParseTuple(args, "si|s", &mode, &n, &pixel_format)) {
return NULL;
}

switch (n) {
case 1: /* BC1: 565 color, 1-bit alpha */
case 2: /* BC2: 565 color, 4-bit alpha */
case 3: /* BC3: 565 color, 2-endpoint 8-bit interpolated alpha */
case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */
case 7: /* BC7: 4-channel 8-bit via everything */
actual = "RGBA";
break;
case 4: /* BC4: 1-channel 8-bit via 1 BC3 alpha block */
actual = "L";
break;
case 5: /* BC5: 2-channel 8-bit via 2 BC3 alpha blocks */
actual = "RGB";
break;
case 6: /* BC6: 3-channel 16-bit float */
/* TODO: support 4-channel floating point images */
actual = "RGBAF";
Expand All @@ -389,14 +392,14 @@ PyImaging_BcnDecoderNew(PyObject *self, PyObject *args) {
return NULL;
}

decoder = PyImaging_DecoderNew(0);
decoder = PyImaging_DecoderNew(sizeof(char *));
if (decoder == NULL) {
return NULL;
}

decoder->decode = ImagingBcnDecode;
decoder->state.state = n;
decoder->state.ystep = ystep;
((BCNSTATE *)decoder->state.context)->pixel_format = pixel_format;

return (PyObject *)decoder;
}
Expand Down
3 changes: 3 additions & 0 deletions src/libImaging/Bcn.h
@@ -0,0 +1,3 @@
typedef struct {
char *pixel_format;
} BCNSTATE;
77 changes: 49 additions & 28 deletions src/libImaging/BcnDecode.c
Expand Up @@ -13,6 +13,8 @@

#include "Imaging.h"

#include "Bcn.h"

typedef struct {
UINT8 r, g, b, a;
} rgba;
Expand All @@ -35,6 +37,11 @@ typedef struct {
UINT8 lut[6];
} bc3_alpha;

typedef struct {
INT8 a0, a1;
UINT8 lut[6];
} bc5s_alpha;

#define LOAD16(p) (p)[0] | ((p)[1] << 8)

#define LOAD32(p) (p)[0] | ((p)[1] << 8) | ((p)[2] << 16) | ((p)[3] << 24)
Expand All @@ -46,11 +53,6 @@ bc1_color_load(bc1_color *dst, const UINT8 *src) {
dst->lut = LOAD32(src + 4);
}

static void
bc3_alpha_load(bc3_alpha *dst, const UINT8 *src) {
memcpy(dst, src, sizeof(bc3_alpha));
}

static rgba
decode_565(UINT16 x) {
rgba c;
Expand Down Expand Up @@ -113,15 +115,26 @@ decode_bc1_color(rgba *dst, const UINT8 *src, int separate_alpha) {
}

static void
decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o) {
bc3_alpha b;
decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o, int sign) {
UINT16 a0, a1;
UINT8 a[8];
int n, lut, aw;
bc3_alpha_load(&b, src);
int n, lut1, lut2, aw;
if (sign == 1) {
bc5s_alpha b;
memcpy(&b, src, sizeof(bc5s_alpha));
a0 = (b.a0 + 255) / 2;
a1 = (b.a1 + 255) / 2;
lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16);
lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16);
} else {
bc3_alpha b;
memcpy(&b, src, sizeof(bc3_alpha));
a0 = b.a0;
a1 = b.a1;
lut1 = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16);
lut2 = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16);
}

a0 = b.a0;
a1 = b.a1;
a[0] = (UINT8)a0;
a[1] = (UINT8)a1;
if (a0 > a1) {
Expand All @@ -139,14 +152,12 @@ decode_bc3_alpha(char *dst, const UINT8 *src, int stride, int o) {
a[6] = 0;
a[7] = 0xff;
}
lut = b.lut[0] | (b.lut[1] << 8) | (b.lut[2] << 16);
for (n = 0; n < 8; n++) {
aw = 7 & (lut >> (3 * n));
aw = 7 & (lut1 >> (3 * n));
dst[stride * n + o] = a[aw];
}
lut = b.lut[3] | (b.lut[4] << 8) | (b.lut[5] << 16);
for (n = 0; n < 8; n++) {
aw = 7 & (lut >> (3 * n));
aw = 7 & (lut2 >> (3 * n));
dst[stride * (8 + n) + o] = a[aw];
}
}
Expand All @@ -172,18 +183,18 @@ decode_bc2_block(rgba *col, const UINT8 *src) {
static void
decode_bc3_block(rgba *col, const UINT8 *src) {
decode_bc1_color(col, src + 8, 1);
decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3);
decode_bc3_alpha((char *)col, src, sizeof(col[0]), 3, 0);
}

static void
decode_bc4_block(lum *col, const UINT8 *src) {
decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0);
decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, 0);
}

static void
decode_bc5_block(rgba *col, const UINT8 *src) {
decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0);
decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1);
decode_bc5_block(rgba *col, const UINT8 *src, int sign) {
decode_bc3_alpha((char *)col, src, sizeof(col[0]), 0, sign);
decode_bc3_alpha((char *)col, src + 8, sizeof(col[0]), 1, sign);
}

/* BC6 and BC7 are described here:
Expand Down Expand Up @@ -813,7 +824,7 @@ put_block(Imaging im, ImagingCodecState state, const char *col, int sz, int C) {

static int
decode_bcn(
Imaging im, ImagingCodecState state, const UINT8 *src, int bytes, int N, int C) {
Imaging im, ImagingCodecState state, const UINT8 *src, int bytes, int N, int C, char *pixel_format) {
int ymax = state->ysize + state->yoff;
const UINT8 *ptr = src;
switch (N) {
Expand All @@ -836,7 +847,19 @@ decode_bcn(
DECODE_LOOP(2, 16, rgba);
DECODE_LOOP(3, 16, rgba);
DECODE_LOOP(4, 8, lum);
DECODE_LOOP(5, 16, rgba);
case 5:
while (bytes >= 16) {
rgba col[16];
memset(col, 0, 16 * sizeof(col[0]));
decode_bc5_block(col, ptr, strcmp(pixel_format, "BC5S") == 0 ? 1 : 0);
put_block(im, state, (const char *)col, sizeof(col[0]), C);
ptr += 16;
bytes -= 16;
if (state->y >= ymax) {
return -1;
}
}
break;
case 6:
while (bytes >= 16) {
rgb32f col[16];
Expand All @@ -849,7 +872,7 @@ decode_bcn(
}
}
break;
DECODE_LOOP(7, 16, rgba);
DECODE_LOOP(7, 16, rgba);
#undef DECODE_LOOP
}
return (int)(ptr - src);
Expand All @@ -860,9 +883,7 @@ ImagingBcnDecode(Imaging im, ImagingCodecState state, UINT8 *buf, Py_ssize_t byt
int N = state->state & 0xf;
int width = state->xsize;
int height = state->ysize;
if ((width & 3) | (height & 3)) {
return decode_bcn(im, state, buf, bytes, N, 1);
} else {
return decode_bcn(im, state, buf, bytes, N, 0);
}
int C = (width & 3) | (height & 3) ? 1 : 0;
char *pixel_format = ((BCNSTATE *)state->context)->pixel_format;
return decode_bcn(im, state, buf, bytes, N, C, pixel_format);
}