From adaa70357662a11cd4b7c0beddaad4e92164c5d9 Mon Sep 17 00:00:00 2001 From: Jerome Leclanche Date: Sat, 17 Feb 2018 02:46:29 +0200 Subject: [PATCH] Add support for BLP file format --- CHANGES.rst | 2 +- Tests/images/blp2_dxt1.blp | Bin 0 -> 33940 bytes Tests/images/blp2_dxt1.png | Bin 0 -> 761 bytes Tests/test_file_blp.py | 14 ++ src/PIL/BlpImagePlugin.py | 432 +++++++++++++++++++++++++++++++++++++ src/PIL/__init__.py | 5 +- 6 files changed, 450 insertions(+), 3 deletions(-) create mode 100644 Tests/images/blp2_dxt1.blp create mode 100644 Tests/images/blp2_dxt1.png create mode 100644 Tests/test_file_blp.py create mode 100644 src/PIL/BlpImagePlugin.py diff --git a/CHANGES.rst b/CHANGES.rst index 5d2f4e7a6..e9d53d563 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1189,7 +1189,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/blp2_dxt1.blp b/Tests/images/blp2_dxt1.blp new file mode 100644 index 0000000000000000000000000000000000000000..73c0c91b51ad578c142ae9da8db4df31ad48f7d2 GIT binary patch literal 33940 zcmeIy!3}^Q3uamjv<@tCMK zF>6!;5?DHcUi$fEIptsv`~wbffCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8 zaDW3G-~b0WzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0W zzyS_$fCC)h00%h00S<70103K02ROh14sd`29N+*4IKTl8aDW3G-~b0WzyS_$fCC)h k00%h00S<70103K02ROh14sd`29N+*4IKTl8aA0Kzekgc{&Hw-a literal 0 HcmV?d00001 diff --git a/Tests/images/blp2_dxt1.png b/Tests/images/blp2_dxt1.png new file mode 100644 index 0000000000000000000000000000000000000000..f2a24618a9acde300dc2cc345cc1c0a8bca92cdb GIT binary patch literal 761 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911L)MWvCLk0$>1D-C9Ar*7pURGpeP+&M< zpdgev;fuc`&ynpc=JS8wVGMm;&hRIN;S!I-EJhDwhDT!W2FQRX a23z}VrdL&`^38xrn!(f6&t;ucLK6U{y4X$t literal 0 HcmV?d00001 diff --git a/Tests/test_file_blp.py b/Tests/test_file_blp.py new file mode 100644 index 000000000..febba73c2 --- /dev/null +++ b/Tests/test_file_blp.py @@ -0,0 +1,14 @@ +from PIL import Image + +from helper import PillowTestCase, unittest + + +class TestFileBlp(PillowTestCase): + def test_load_blp2_dxt1(self): + im = Image.open("Tests/images/blp2_dxt1.blp") + target = Image.open("Tests/images/blp2_dxt1.png") + self.assert_image_similar(im, target.convert("RGBA"), 15) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py new file mode 100644 index 000000000..72bf70482 --- /dev/null +++ b/src/PIL/BlpImagePlugin.py @@ -0,0 +1,432 @@ +""" +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 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 + final = (bytearray(), bytearray(), bytearray(), bytearray()) + + for block in range(blocks): + # Decode next 8-byte block. + idx = block * 8 + color0, color1, bits = struct.unpack("> 11) & 0x1f) << 3 + g0 = ((color0 >> 5) & 0x3f) << 2 + b0 = (color0 & 0x1f) << 3 + + # color 1, packed 5-6-5 + r1 = ((color1 >> 11) & 0x1f) << 3 + g1 = ((color1 >> 5) & 0x3f) << 2 + b1 = (color1 & 0x1f) << 3 + + # Decode this block into 4x4 pixels + # Accumulate the results onto our 4 row accumulators + 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: + final[j].append(r0) + final[j].append(g0) + final[j].append(b0) + elif control == 1: + final[j].append(r1) + final[j].append(g1) + final[j].append(b1) + elif control == 2: + if color0 > color1: + final[j].append((2 * r0 + r1) // 3) + final[j].append((2 * g0 + g1) // 3) + final[j].append((2 * b0 + b1) // 3) + else: + final[j].append((r0 + r1) // 2) + final[j].append((g0 + g1) // 2) + final[j].append((b0 + b1) // 2) + elif control == 3: + if color0 > color1: + final[j].append((2 * r1 + r0) // 3) + final[j].append((2 * g1 + g0) // 3) + final[j].append((2 * b1 + b0) // 3) + else: + final[j].append(0) + final[j].append(0) + final[j].append(0) + if alpha: + final[j].append(0) + + if alpha: + final[j].append(0xFF) + + return final + + +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 + final = (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("> 11) & 0x1f) << 3 + g0 = ((color0 >> 5) & 0x3f) << 2 + b0 = (color0 & 0x1f) << 3 + + # color 1, packed 5-6-5 + r1 = ((color1 >> 11) & 0x1f) << 3 + g1 = ((color1 >> 5) & 0x3f) << 2 + b1 = (color1 & 0x1f) << 3 + + for j in range(4): + high = False # Do we want the higher bits? + for i in range(4): + alphacode_index = (4 * j + i) // 2 + a = bits[alphacode_index] + if high: + high = False + a >>= 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: + final[j].append(r0) + final[j].append(g0) + final[j].append(b0) + final[j].append(a) + elif color_code == 1: + final[j].append(r1) + final[j].append(g1) + final[j].append(b1) + final[j].append(a) + elif color_code == 2: + final[j].append((2 * r0 + r1) // 3) + final[j].append((2 * g0 + g1) // 3) + final[j].append((2 * b0 + b1) // 3) + final[j].append(a) + elif color_code == 3: + final[j].append((2 * r1 + r0) // 3) + final[j].append((2 * g1 + g0) // 3) + final[j].append((2 * b1 + b0) // 3) + final[j].append(a) + + return final + + +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 + final = (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("> 11) & 0x1f) << 3 + g0 = ((color0 >> 5) & 0x3f) << 2 + b0 = (color0 & 0x1f) << 3 + + # color 1, packed 5-6-5 + r1 = ((color1 >> 11) & 0x1f) << 3 + g1 = ((color1 >> 5) & 0x3f) << 2 + b1 = (color1 & 0x1f) << 3 + + for j in range(4): + for i in range(4): + # get next control op and generate a pixel + alphacode_index = 3 * (4 * j + i) + + if alphacode_index <= 12: + alphacode = (alphacode2 >> 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 + else: + if 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: + final[j].append(r0) + final[j].append(g0) + final[j].append(b0) + final[j].append(a) + elif color_code == 1: + final[j].append(r1) + final[j].append(g1) + final[j].append(b1) + final[j].append(a) + elif color_code == 2: + final[j].append((2 * r0 + r1) // 3) + final[j].append((2 * g0 + g1) // 3) + final[j].append((2 * b0 + b1) // 3) + final[j].append(a) + elif color_code == 3: + final[j].append((2 * r1 + r0) // 3) + final[j].append((2 * g1 + g0) // 3) + final[j].append((2 * b1 + b0) // 3) + final[j].append(a) + + return tuple(final) + + +def getpalette(data): + """ + Helper to transform a BytesIO object into a palette + """ + palette = [] + string = BytesIO(data) + while True: + try: + palette.append(struct.unpack("<4B", string.read(4))) + except struct.error: + break + return palette + + +class BLPFormatError(NotImplementedError): + pass + + +class BlpImageFile(ImageFile.ImageFile): + """ + Blizzard Mipmap Format + """ + format = "BLP" + format_description = "Blizzard Mipmap Format" + + def _decode_blp1(self): + header = BytesIO(self.fp.read(28 + 16 * 4 + 16 * 4)) + + magic, compression = struct.unpack("<4si", header.read(8)) + encoding, alpha_depth, alpha_encoding, has_mips = struct.unpack( + "<4b", header.read(4) + ) + self.size = struct.unpack("