Skip to content

Commit

Permalink
Merge 2be760c into 8846e99
Browse files Browse the repository at this point in the history
  • Loading branch information
robert-nix committed Aug 26, 2016
2 parents 8846e99 + 2be760c commit a556810
Show file tree
Hide file tree
Showing 14 changed files with 987 additions and 154 deletions.
156 changes: 30 additions & 126 deletions PIL/DdsImagePlugin.py
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
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
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
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.
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
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
@@ -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
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
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

0 comments on commit a556810

Please sign in to comment.