Skip to content
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
156 changes: 30 additions & 126 deletions PIL/DdsImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,118 +93,11 @@
DXT5_FOURCC = 0x35545844


def _decode565(bits):
a = ((bits >> 11) & 0x1f) << 3
b = ((bits >> 5) & 0x3f) << 2
c = (bits & 0x1f) << 3
return a, b, c


def _c2a(a, b):
return (2 * a + b) // 3


def _c2b(a, b):
return (a + b) // 2


def _c3(a, b):
return (2 * b + a) // 3


def _dxt1(data, width, height):
# TODO implement this function as pixel format in decode.c
ret = bytearray(4 * width * height)

for y in range(0, height, 4):
for x in range(0, width, 4):
color0, color1, bits = struct.unpack("<HHI", data.read(8))

r0, g0, b0 = _decode565(color0)
r1, g1, b1 = _decode565(color1)

# Decode this block into 4x4 pixels
for j in range(4):
for i in range(4):
# get next control op and generate a pixel
control = bits & 3
bits = bits >> 2
if control == 0:
r, g, b = r0, g0, b0
elif control == 1:
r, g, b = r1, g1, b1
elif control == 2:
if color0 > color1:
r, g, b = _c2a(r0, r1), _c2a(g0, g1), _c2a(b0, b1)
else:
r, g, b = _c2b(r0, r1), _c2b(g0, g1), _c2b(b0, b1)
elif control == 3:
if color0 > color1:
r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1)
else:
r, g, b = 0, 0, 0

idx = 4 * ((y + j) * width + (x + i))
ret[idx:idx+4] = struct.pack('4B', r, g, b, 255)

return bytes(ret)


def _dxtc_alpha(a0, a1, ac0, ac1, ai):
if ai <= 12:
ac = (ac0 >> ai) & 7
elif ai == 15:
ac = (ac0 >> 15) | ((ac1 << 1) & 6)
else:
ac = (ac1 >> (ai - 16)) & 7

if ac == 0:
alpha = a0
elif ac == 1:
alpha = a1
elif a0 > a1:
alpha = ((8 - ac) * a0 + (ac - 1) * a1) // 7
elif ac == 6:
alpha = 0
elif ac == 7:
alpha = 0xff
else:
alpha = ((6 - ac) * a0 + (ac - 1) * a1) // 5

return alpha


def _dxt5(data, width, height):
# TODO implement this function as pixel format in decode.c
ret = bytearray(4 * width * height)

for y in range(0, height, 4):
for x in range(0, width, 4):
a0, a1, ac0, ac1, c0, c1, code = struct.unpack("<2BHI2HI",
data.read(16))

r0, g0, b0 = _decode565(c0)
r1, g1, b1 = _decode565(c1)

for j in range(4):
for i in range(4):
ai = 3 * (4 * j + i)
alpha = _dxtc_alpha(a0, a1, ac0, ac1, ai)

cc = (code >> 2 * (4 * j + i)) & 3
if cc == 0:
r, g, b = r0, g0, b0
elif cc == 1:
r, g, b = r1, g1, b1
elif cc == 2:
r, g, b = _c2a(r0, r1), _c2a(g0, g1), _c2a(b0, b1)
elif cc == 3:
r, g, b = _c3(r0, r1), _c3(g0, g1), _c3(b0, b1)

idx = 4 * ((y + j) * width + (x + i))
ret[idx:idx+4] = struct.pack('4B', r, g, b, alpha)

return bytes(ret)
# dxgiformat.h

DXGI_FORMAT_BC7_TYPELESS = 97
DXGI_FORMAT_BC7_UNORM = 98
DXGI_FORMAT_BC7_UNORM_SRGB = 99


class DdsImageFile(ImageFile.ImageFile):
Expand Down Expand Up @@ -233,28 +126,39 @@ def _open(self):
bitcount, rmask, gmask, bmask, amask = struct.unpack("<5I",
header.read(20))

self.tile = [
("raw", (0, 0) + self.size, 0, (self.mode, 0, 1))
]

data_start = header_size + 4
n = 0
if fourcc == b"DXT1":
self.pixel_format = "DXT1"
codec = _dxt1
n = 1
elif fourcc == b"DXT3":
self.pixel_format = "DXT3"
n = 2
elif fourcc == b"DXT5":
self.pixel_format = "DXT5"
codec = _dxt5
n = 3
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):
self.pixel_format = "BC7"
n = 7
elif dxgi_format == DXGI_FORMAT_BC7_UNORM_SRGB:
self.pixel_format = "BC7"
self.im_info["gamma"] = 1/2.2
n = 7
else:
raise NotImplementedError("Unimplemented DXGI format %d" %
(dxgi_format))
else:
raise NotImplementedError("Unimplemented pixel format %r" %
(fourcc))

try:
decoded_data = codec(self.fp, self.width, self.height)
except struct.error:
raise IOError("Truncated DDS file")
finally:
self.fp.close()

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

def load_seek(self, pos):
pass
Expand Down
5 changes: 2 additions & 3 deletions PIL/FtexImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
import struct
from io import BytesIO
from PIL import Image, ImageFile
from PIL.DdsImagePlugin import _dxt1


MAGIC = b"FTEX"
Expand Down Expand Up @@ -73,8 +72,8 @@ def _open(self):
data = self.fp.read(mipmap_size)

if format == FORMAT_DXT1:
data = _dxt1(BytesIO(data), self.width, self.height)
self.tile = [("raw", (0, 0) + self.size, 0, ('RGBX', 0, 1))]
self.mode = "RGBA"
self.tile = [("bcn", (0, 0) + self.size, 0, (1))]
elif format == FORMAT_UNCOMPRESSED:
self.tile = [("raw", (0, 0) + self.size, 0, ('RGB', 0, 1))]
else:
Expand Down
Binary file added Tests/images/bc7-argb-8bpp_MipMaps-1.dds
Binary file not shown.
Binary file added Tests/images/bc7-argb-8bpp_MipMaps-1.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 modified Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.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 modified Tests/images/dxt3-argb-8bbp-explicitalpha_MipMaps-1.dds
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 30 additions & 12 deletions Tests/test_file_dds.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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_BC7 = "Tests/images/bc7-argb-8bpp_MipMaps-1.dds"


class TestFileDds(PillowTestCase):
Expand All @@ -22,7 +23,6 @@ def test_sanity_dxt1(self):
self.assertEqual(im.mode, "RGBA")
self.assertEqual(im.size, (256, 256))

# This target image is from the test set of images, and is exact.
self.assert_image_equal(target.convert('RGBA'), im)

def test_sanity_dxt5(self):
Expand All @@ -37,18 +37,35 @@ def test_sanity_dxt5(self):
self.assertEqual(im.mode, "RGBA")
self.assertEqual(im.size, (256, 256))

# Imagemagick, which generated this target image from the .dds
# has a slightly different decoder than is standard. It looks
# a little brighter. The 0,0 pixel is (00,6c,f8,ff) by our code,
# and by the target image for the DXT1, and the imagemagick .png
# is giving (00, 6d, ff, ff). So, assert similar, pretty tight
# I'm currently seeing about a 3 for the epsilon.
self.assert_image_similar(target, im, 5)
self.assert_image_equal(target, im)

def test_sanity_dxt3(self):
"""Check DXT3 images are not supported"""
self.assertRaises(NotImplementedError,
lambda: Image.open(TEST_FILE_DXT3))
"""Check DXT3 images can be opened"""

target = Image.open(TEST_FILE_DXT3.replace('.dds', '.png'))

im = Image.open(TEST_FILE_DXT3)
im.load()

self.assertEqual(im.format, "DDS")
self.assertEqual(im.mode, "RGBA")
self.assertEqual(im.size, (256, 256))

self.assert_image_equal(target, im)

def test_dx10_bc7(self):
"""Check DX10 images can be opened"""

target = Image.open(TEST_FILE_DX10_BC7.replace('.dds', '.png'))

im = Image.open(TEST_FILE_DX10_BC7)
im.load()

self.assertEqual(im.format, "DDS")
self.assertEqual(im.mode, "RGBA")
self.assertEqual(im.size, (256, 256))

self.assert_image_equal(target, im)

def test__validate_true(self):
"""Check valid prefix"""
Expand Down Expand Up @@ -89,7 +106,8 @@ def test_short_file(self):
img_file = f.read()

def short_file():
Image.open(BytesIO(img_file[:-100]))
im = Image.open(BytesIO(img_file[:-100]))
im.load()

self.assertRaises(IOError, short_file)

Expand Down
7 changes: 5 additions & 2 deletions Tests/test_file_ftex.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from helper import PillowTestCase
from helper import unittest, PillowTestCase
from PIL import Image


Expand All @@ -13,4 +13,7 @@ def test_load_raw(self):
def test_load_dxt1(self):
im = Image.open('Tests/images/ftex_dxt1.ftc')
target = Image.open('Tests/images/ftex_dxt1.png')
self.assert_image_similar(im, target, 15)
self.assert_image_similar(im, target.convert('RGBA'), 15)

if __name__ == '__main__':
unittest.main()
2 changes: 2 additions & 0 deletions _imaging.c
Original file line number Diff line number Diff line change
Expand Up @@ -3297,6 +3297,7 @@ static PyTypeObject PixelAccess_Type = {
pluggable codecs, but not before PIL 1.2 */

/* Decoders (in decode.c) */
extern PyObject* PyImaging_BcnDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_BitDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_FliDecoderNew(PyObject* self, PyObject* args);
extern PyObject* PyImaging_GifDecoderNew(PyObject* self, PyObject* args);
Expand Down Expand Up @@ -3362,6 +3363,7 @@ static PyMethodDef functions[] = {
{"copy", (PyCFunction)_copy2, 1},

/* Codecs */
{"bcn_decoder", (PyCFunction)PyImaging_BcnDecoderNew, 1},
{"bit_decoder", (PyCFunction)PyImaging_BitDecoderNew, 1},
{"eps_encoder", (PyCFunction)PyImaging_EpsEncoderNew, 1},
{"fli_decoder", (PyCFunction)PyImaging_FliDecoderNew, 1},
Expand Down
50 changes: 50 additions & 0 deletions decode.c
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,56 @@ PyImaging_BitDecoderNew(PyObject* self, PyObject* args)
}


/* -------------------------------------------------------------------- */
/* BCn: GPU block-compressed texture formats */
/* -------------------------------------------------------------------- */

PyObject*
PyImaging_BcnDecoderNew(PyObject* self, PyObject* args)
{
ImagingDecoderObject* decoder;

char* mode;
char* actual;
int n = 0;
int ystep = 1;
if (!PyArg_ParseTuple(args, "s|ii", &mode, &n, &ystep))
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 6: /* BC6: 3-channel 16-bit float */
/* TODO: support 4-channel floating point images */
actual = "RGBAF"; break;
default:
PyErr_SetString(PyExc_ValueError, "block compression type unknown");
return NULL;
}

if (strcmp(mode, actual) != 0) {
PyErr_SetString(PyExc_ValueError, "bad image mode");
return NULL;
}

decoder = PyImaging_DecoderNew(0);
if (decoder == NULL)
return NULL;

decoder->decode = ImagingBcnDecode;
decoder->state.state = n;
decoder->state.ystep = ystep;

return (PyObject*) decoder;
}


/* -------------------------------------------------------------------- */
/* FLI */
/* -------------------------------------------------------------------- */
Expand Down
Loading