diff --git a/CHANGES.rst b/CHANGES.rst index 96a7870ae..bb5965e5f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1198,7 +1198,7 @@ Changelog (Pillow) - Change function declaration to match Tcl_CmdProc type #1966 [homm] -- Integer overflow checks on all calls to *alloc #1781 +- Integer overflow checks on all calls to \*alloc #1781 [wiredfool] - Change equals method on Image so it short circuits #1967 diff --git a/Tests/images/blp/blp1_jpeg.blp b/Tests/images/blp/blp1_jpeg.blp new file mode 100644 index 000000000..bdf7146ed Binary files /dev/null and b/Tests/images/blp/blp1_jpeg.blp differ diff --git a/Tests/images/blp/blp2_dxt1.blp b/Tests/images/blp/blp2_dxt1.blp new file mode 100644 index 000000000..73c0c91b5 Binary files /dev/null and b/Tests/images/blp/blp2_dxt1.blp differ diff --git a/Tests/images/blp/blp2_dxt1.png b/Tests/images/blp/blp2_dxt1.png new file mode 100644 index 000000000..f2a24618a Binary files /dev/null and b/Tests/images/blp/blp2_dxt1.png differ diff --git a/Tests/images/blp/blp2_dxt1a.blp b/Tests/images/blp/blp2_dxt1a.blp new file mode 100644 index 000000000..5bedc27d6 Binary files /dev/null and b/Tests/images/blp/blp2_dxt1a.blp differ diff --git a/Tests/images/blp/blp2_dxt1a.png b/Tests/images/blp/blp2_dxt1a.png new file mode 100644 index 000000000..d2cdea807 Binary files /dev/null and b/Tests/images/blp/blp2_dxt1a.png differ diff --git a/Tests/images/blp/blp2_raw.blp b/Tests/images/blp/blp2_raw.blp new file mode 100644 index 000000000..813d4bfae Binary files /dev/null and b/Tests/images/blp/blp2_raw.blp differ diff --git a/Tests/images/blp/blp2_raw.png b/Tests/images/blp/blp2_raw.png new file mode 100644 index 000000000..c77a3c048 Binary files /dev/null and b/Tests/images/blp/blp2_raw.png differ diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py new file mode 100644 index 000000000..e0530cb1f --- /dev/null +++ b/Tests/test_file_blp.py @@ -0,0 +1,24 @@ +from PIL import Image + +from helper import PillowTestCase, unittest + + +class TestFileBlp(PillowTestCase): + def test_load_blp2_raw(self): + im = Image.open("Tests/images/blp/blp2_raw.blp") + target = Image.open("Tests/images/blp/blp2_raw.png") + self.assert_image_equal(im, target) + + def test_load_blp2_dxt1(self): + im = Image.open("Tests/images/blp/blp2_dxt1.blp") + target = Image.open("Tests/images/blp/blp2_dxt1.png") + self.assert_image_equal(im, target) + + def test_load_blp2_dxt1a(self): + im = Image.open("Tests/images/blp/blp2_dxt1a.blp") + target = Image.open("Tests/images/blp/blp2_dxt1a.png") + self.assert_image_equal(im, target) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py new file mode 100644 index 000000000..ec358db3b --- /dev/null +++ b/src/PIL/BlpImagePlugin.py @@ -0,0 +1,435 @@ +""" +Blizzard Mipmap Format (.blp) +Jerome Leclanche + +The contents of this file are hereby released in the public domain (CC0) +Full text of the CC0 license: + https://creativecommons.org/publicdomain/zero/1.0/ + +BLP1 files, used mostly in Warcraft III, are not fully supported. +All types of BLP2 files used in World of Warcraft are supported. + +The BLP file structure consists of a header, up to 16 mipmaps of the +texture + +Texture sizes must be powers of two, though the two dimensions do +not have to be equal; 512x256 is valid, but 512x200 is not. +The first mipmap (mipmap #0) is the full size image; each subsequent +mipmap halves both dimensions. The final mipmap should be 1x1. + +BLP files come in many different flavours: +* JPEG-compressed (type == 0) - only supported for BLP1. +* RAW images (type == 1, encoding == 1). Each mipmap is stored as an + array of 8-bit values, one per pixel, left to right, top to bottom. + Each value is an index to the palette. +* DXT-compressed (type == 1, encoding == 2): +- DXT1 compression is used if alpha_encoding == 0. + - An additional alpha bit is used if alpha_depth == 1. + - DXT3 compression is used if alpha_encoding == 1. + - DXT5 compression is used if alpha_encoding == 7. +""" + +import struct +from io import BytesIO + +from . import Image, ImageFile + + +BLP_FORMAT_JPEG = 0 + +BLP_ENCODING_UNCOMPRESSED = 1 +BLP_ENCODING_DXT = 2 +BLP_ENCODING_UNCOMPRESSED_RAW_BGRA = 3 + +BLP_ALPHA_ENCODING_DXT1 = 0 +BLP_ALPHA_ENCODING_DXT3 = 1 +BLP_ALPHA_ENCODING_DXT5 = 7 + + +def unpack_565(i): + return ( + ((i >> 11) & 0x1f) << 3, + ((i >> 5) & 0x3f) << 2, + (i & 0x1f) << 3 + ) + + +def decode_dxt1(data, alpha=False): + """ + input: one "row" of data (i.e. will produce 4*width pixels) + """ + + blocks = len(data) // 8 # number of blocks in row + ret = (bytearray(), bytearray(), bytearray(), bytearray()) + + for block in range(blocks): + # Decode next 8-byte block. + idx = block * 8 + color0, color1, bits = struct.unpack("> 2 + + a = 0xFF + 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 = (2 * r0 + r1) // 3 + g = (2 * g0 + g1) // 3 + b = (2 * b0 + b1) // 3 + else: + r = (r0 + r1) // 2 + g = (g0 + g1) // 2 + b = (b0 + b1) // 2 + elif control == 3: + if color0 > color1: + r = (2 * r1 + r0) // 3 + g = (2 * g1 + g0) // 3 + b = (2 * b1 + b0) // 3 + else: + r, g, b, a = 0, 0, 0, 0 + + if alpha: + ret[j].extend([r, g, b, a]) + else: + ret[j].extend([r, g, b]) + + return ret + + +def decode_dxt3(data): + """ + input: one "row" of data (i.e. will produce 4*width pixels) + """ + + blocks = len(data) // 16 # number of blocks in row + ret = (bytearray(), bytearray(), bytearray(), bytearray()) + + for block in range(blocks): + idx = block * 16 + block = data[idx:idx + 16] + # Decode next 16-byte block. + bits = struct.unpack("<8B", block[:8]) + color0, color1 = struct.unpack(">= 4 + else: + high = True + a &= 0xf + a *= 17 # We get a value between 0 and 15 + + color_code = (code >> 2 * (4 * j + i)) & 0x03 + + if color_code == 0: + r, g, b = r0, g0, b0 + elif color_code == 1: + r, g, b = r1, g1, b1 + elif color_code == 2: + r = (2 * r0 + r1) // 3 + g = (2 * g0 + g1) // 3 + b = (2 * b0 + b1) // 3 + elif color_code == 3: + r = (2 * r1 + r0) // 3 + g = (2 * g1 + g0) // 3 + b = (2 * b1 + b0) // 3 + + ret[j].extend([r, g, b, a]) + + return ret + + +def decode_dxt5(data): + """ + input: one "row" of data (i.e. will produce 4 * width pixels) + """ + + blocks = len(data) // 16 # number of blocks in row + ret = (bytearray(), bytearray(), bytearray(), bytearray()) + + for block in range(blocks): + idx = block * 16 + block = data[idx:idx + 16] + # Decode next 16-byte block. + a0, a1 = struct.unpack("> alphacode_index) & 0x07 + elif alphacode_index == 15: + alphacode = (alphacode2 >> 15) | ((alphacode1 << 1) & 0x06) + else: # alphacode_index >= 18 and alphacode_index <= 45 + alphacode = (alphacode1 >> (alphacode_index - 16)) & 0x07 + + if alphacode == 0: + a = a0 + elif alphacode == 1: + a = a1 + elif a0 > a1: + a = ((8 - alphacode) * a0 + (alphacode - 1) * a1) // 7 + elif alphacode == 6: + a = 0 + elif alphacode == 7: + a = 255 + else: + a = ((6 - alphacode) * a0 + (alphacode - 1) * a1) // 5 + + color_code = (code >> 2 * (4 * j + i)) & 0x03 + + if color_code == 0: + r, g, b = r0, g0, b0 + elif color_code == 1: + r, g, b = r1, g1, b1 + elif color_code == 2: + r = (2 * r0 + r1) // 3 + g = (2 * g0 + g1) // 3 + b = (2 * b0 + b1) // 3 + elif color_code == 3: + r = (2 * r1 + r0) // 3 + g = (2 * g1 + g0) // 3 + b = (2 * b1 + b0) // 3 + + ret[j].extend([r, g, b, a]) + + return ret + + +class BLPFormatError(NotImplementedError): + pass + + +class BlpImageFile(ImageFile.ImageFile): + """ + Blizzard Mipmap Format + """ + format = "BLP" + format_description = "Blizzard Mipmap Format" + + def _open(self): + self.magic = self.fp.read(4) + self._read_blp_header() + + if self.magic == b"BLP1": + decoder = "BLP1" + self.mode = "RGB" + elif self.magic == b"BLP2": + decoder = "BLP2" + self.mode = "RGBA" if self._blp_alpha_depth else "RGB" + else: + raise BLPFormatError("Bad BLP magic %r" % (self.magic)) + + self.tile = [ + (decoder, (0, 0) + self.size, 0, (self.mode, 0, 1)) + ] + + def _read_blp_header(self): + self._blp_compression, = struct.unpack("