From 0cb12bd2f5d640064dacaab84aaaf82d93fc881e Mon Sep 17 00:00:00 2001 From: Joey Payne Date: Sun, 2 Nov 2014 22:54:38 +1300 Subject: [PATCH] Added custom icon support so PIL replaces ImageMagick. --- bmpimageplugin.py | 327 +++++++++++++++++++++++++++++++++++++++++ ico_plugin.py | 360 ++++++++++++++++++++++++++++++++++++++++++++++ pe.py | 47 ++++-- 3 files changed, 720 insertions(+), 14 deletions(-) create mode 100644 bmpimageplugin.py create mode 100644 ico_plugin.py diff --git a/bmpimageplugin.py b/bmpimageplugin.py new file mode 100644 index 0000000..523e52b --- /dev/null +++ b/bmpimageplugin.py @@ -0,0 +1,327 @@ +# +# The Python Imaging Library. +# $Id$ +# +# BMP file handler +# +# Windows (and OS/2) native bitmap storage format. +# +# history: +# 1995-09-01 fl Created +# 1996-04-30 fl Added save +# 1997-08-27 fl Fixed save of 1-bit images +# 1998-03-06 fl Load P images as L where possible +# 1998-07-03 fl Load P images as 1 where possible +# 1998-12-29 fl Handle small palettes +# 2002-12-30 fl Fixed load of 1-bit palette images +# 2003-04-21 fl Fixed load of 1-bit monochrome images +# 2003-04-23 fl Added limited support for BI_BITFIELDS compression +# +# Copyright (c) 1997-2003 by Secret Labs AB +# Copyright (c) 1995-2003 by Fredrik Lundh +# +# See the README file for information on usage and redistribution. +# + + +__version__ = "0.7" + + +from PIL import Image, ImageFile, ImagePalette, _binary +from PIL.BmpImagePlugin import * #This is a hack to override the default bmp plugin for PIL +from cStringIO import StringIO +import math + +i8 = _binary.i8 +i16 = _binary.i16le +i32 = _binary.i32le +o8 = _binary.o8 +o16 = _binary.o16le +o32 = _binary.o32le + +# +# -------------------------------------------------------------------- +# Read BMP file + +BIT2MODE = { + # bits => mode, rawmode + 1: ("P", "P;1"), + 4: ("P", "P;4"), + 8: ("P", "P"), + 16: ("RGB", "BGR;15"), + 24: ("RGB", "BGR"), + 32: ("RGBA", "BGRA") +} + + +def _accept(prefix): + return prefix[:2] == b"BM" + + +## +# Image plugin for the Windows BMP format. + +class BmpImageFile(ImageFile.ImageFile): + + format = "BMP" + format_description = "Windows Bitmap" + + def _bitmap(self, header=0, offset=0): + if header: + self.fp.seek(header) + + read = self.fp.read + + if not offset: + offset = self.fp.tell() + # CORE/INFO + s = read(4) + s = s + ImageFile._safe_read(self.fp, i32(s)-4) + + if len(s) == 12: + + # OS/2 1.0 CORE + bits = i16(s[10:]) + self.size = i16(s[4:]), i16(s[6:]) + compression = 0 + lutsize = 3 + colors = 0 + direction = -1 + + elif len(s) in [40, 64, 108, 124]: + + # WIN 3.1 or OS/2 2.0 INFO + bits = i16(s[14:]) + self.size = i32(s[4:]), i32(s[8:]) + compression = i32(s[16:]) + pxperm = (i32(s[24:]), i32(s[28:])) # Pixels per meter + lutsize = 4 + colors = i32(s[32:]) + direction = -1 + if i8(s[11]) == 0xff: + # upside-down storage + self.size = self.size[0], 2**32 - self.size[1] + direction = 0 + + self.info["dpi"] = tuple(map(lambda x: math.ceil(x / 39.3701), + pxperm)) + + else: + raise IOError("Unsupported BMP header type (%d)" % len(s)) + + if (self.size[0]*self.size[1]) > 2**31: + # Prevent DOS for > 2gb images + raise IOError("Unsupported BMP Size: (%dx%d)" % self.size) + + if not colors: + colors = 1 << bits + + # MODE + try: + self.mode, rawmode = BIT2MODE[bits] + except KeyError: + raise IOError("Unsupported BMP pixel depth (%d)" % bits) + + if compression == 3: + # BI_BITFIELDS compression + mask = i32(s[0x36-14:]), i32(s[0x3a-14:]), i32(s[0x3E-14:]), i32(s[0x42-14:]) + if bits == 32 and mask == (0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000): + rawmode = "BGRA" + elif bits == 16 and mask == (0x00f800, 0x0007e0, 0x00001f): + rawmode = "BGR;16" + elif bits == 16 and mask == (0x007c00, 0x0003e0, 0x00001f): + rawmode = "BGR;15" + else: + print bits, map(hex, mask) + raise IOError("Unsupported BMP bitfields layout") + elif compression != 0: + raise IOError("Unsupported BMP compression (%d)" % compression) + + # LUT + if self.mode == "P": + palette = [] + greyscale = 1 + if colors == 2: + indices = (0, 255) + elif colors > 2**16 or colors <= 0: # We're reading a i32. + raise IOError("Unsupported BMP Palette size (%d)" % colors) + else: + indices = list(range(colors)) + for i in indices: + rgb = read(lutsize)[:3] + if rgb != o8(i)*3: + greyscale = 0 + palette.append(rgb) + if greyscale: + if colors == 2: + self.mode = rawmode = "1" + else: + self.mode = rawmode = "L" + else: + self.mode = "P" + self.palette = ImagePalette.raw( + "BGR", b"".join(palette) + ) + + if not offset: + offset = self.fp.tell() + + self.tile = [("raw", + (0, 0) + self.size, + offset, + (rawmode, ((self.size[0]*bits+31) >> 3) & (~3), + direction))] + + self.info["compression"] = compression + + def _open(self): + + # HEAD + s = self.fp.read(14) + if s[:2] != b"BM": + raise SyntaxError("Not a BMP file") + offset = i32(s[10:]) + + self._bitmap(offset=offset) + +class DibImageFile(BmpImageFile): + + format = "DIB" + format_description = "Windows Bitmap" + + def __init__(self, buf, *args, **kwargs): + BmpImageFile.__init__(self, buf, *args, **kwargs) + buf.seek(0) + self.buf = buf.read() + buf.seek(0) + + def _open(self): + self._bitmap() + + def get_file_data(self): + return self.buf + + def to_bitmapimage(self, header): + self.fp.seek(header['offset']) + d = bytearray(self.fp.read(header['size'])) + print self.info + dpi = (96,96) + ppm = tuple(map(lambda x: int(x * 39.3701), dpi)) + + d[24:28] = o32(ppm[0]) + d[28:32] = o32(ppm[1]) + + dib_size = i32(str(d[:4])) + offset = 14 + dib_size + data = StringIO() + data.write(b'BM'+ + o32(14+len(d)) + + o32(0) + + o32(offset)) + + data.write(d) + data.seek(0) + new_image = Image.open(data) + return new_image + +# +# -------------------------------------------------------------------- +# Write BMP file + +SAVE = { + "1": ("1", 1, 2), + "L": ("L", 8, 256), + "P": ("P", 8, 256), + "RGB": ("BGR", 24, 0), + "RGBA": ("BGRA", 32, 0), +} + + +def _save(im, fp, filename, check=0): + try: + rawmode, bits, colors = SAVE[im.mode] + except KeyError: + raise IOError("cannot write mode %s as BMP" % im.mode) + + if check: + return check + + info = im.encoderinfo + + dpi = info.get("dpi", (96, 96)) + + # 1 meter == 39.3701 inches + ppm = tuple(map(lambda x: int(x * 39.3701), dpi)) + + stride = ((im.size[0]*bits+7)//8+3) & (~3) + header = 64 #108 if im.mode == 'RGBA' else 40 # or 64 for OS/2 version 2 + offset = 14 + header + colors*4 + image = stride * im.size[1] + + red_mask = 0x00ff0000 + green_mask = 0x0000ff00 + blue_mask = 0x000000ff + alpha_mask = 0xff000000 + + # bitmap header + fp.write(b"BM" + # file type (magic) + o32(offset+image) + # file size + o32(0) + # reserved + o32(offset)) # image data offset + + width,height = im.size + + # bitmap info header + fp.write(o32(header) + # info header size + o32(width) + # width + o32(height) + # height + o16(1) + # planes + o16(bits) + # depth + o32(3) + # compression (0=uncompressed) + o32(image) + # size of bitmap + o32(ppm[0]) + o32(ppm[1]) + # resolution + o32(colors) + # colors used + o32(colors) + # colors important + o32(red_mask) + # red channel ma + o32(green_mask) + # green channel mask + o32(blue_mask) + # blue channel mask + o32(alpha_mask) + ) + # This was commented out because although it works, some + # decoders do not support images with a BI_BITFIELDS compression + # + #if im.mode == 'RGBA': + # fp.write(o32(red_mask) + # red channel mask + # o32(green_mask) + # green channel mask + # o32(blue_mask) + # blue channel mask + # o32(alpha_mask) + # alpha channel mask + # 'BGRs' + # Color Space + # o8(0)*0x24 + # ciexyztriple color space endpoints + # o32(0) + # red gamma + # o32(0) + # green gamma + # o32(0) # blue gamma + # ) + + fp.write(bytearray(header - 40 - 4)) + + if im.mode == "1": + for i in (0, 255): + fp.write(o8(i) * 4) + elif im.mode == "L": + for i in range(256): + fp.write(o8(i) * 4) + elif im.mode == "P": + fp.write(im.im.getpalette("RGB", "BGRX")) + + #fp.write( + ImageFile._save(im, fp, [("raw", (0, 0)+im.size, 0, + (rawmode, stride, -1))]) + +# +# -------------------------------------------------------------------- +# Registry + +Image.register_open(BmpImageFile.format, BmpImageFile, _accept) +Image.register_save(BmpImageFile.format, _save) + +Image.register_extension(BmpImageFile.format, ".bmp") diff --git a/ico_plugin.py b/ico_plugin.py new file mode 100644 index 0000000..afe37fe --- /dev/null +++ b/ico_plugin.py @@ -0,0 +1,360 @@ +# +# The Python Imaging Library. +# $Id$ +# +# Windows Icon support for PIL +# +# History: +# 96-05-27 fl Created +# +# Copyright (c) Secret Labs AB 1997. +# Copyright (c) Fredrik Lundh 1996. +# +# See the README file for information on usage and redistribution. +# + +# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis +# . +# https://code.google.com/p/casadebender/wiki/Win32IconImagePlugin +# +# Icon format references: +# * http://en.wikipedia.org/wiki/ICO_(file_format) +# * http://msdn.microsoft.com/en-us/library/ms997538.aspx + + +__version__ = "0.1" + +from PIL import Image, ImageFile, BmpImagePlugin, PngImagePlugin, _binary +import bmpimageplugin +from bmpimageplugin import * +from cStringIO import StringIO +from math import log, ceil + +i8 = _binary.i8 +i16 = _binary.i16le +i32 = _binary.i32le +o8 = _binary.o8 +o16 = _binary.o16le +o32 = _binary.o32le + +_MAGIC = b"\0\0\1\0" + + +def _accept(prefix): + return prefix[:4] == _MAGIC + + +class IcoFile: + format = 'ICO' + def __init__(self, buf): + """ + Parse image from file-like object containing ico file data + """ + + # check magic + s = buf.read(6) + if not _accept(s): + raise SyntaxError("not an ICO file") + + self.buf = buf + self.entries = [] + + # Number of items in file + self.nb_items = i16(s[4:]) + + # Get headers for each item + for i in xrange(self.nb_items): + s = buf.read(16) + + icon_header = { + 'width': i8(s[0]), + 'height': i8(s[1]), + 'nb_color': i8(s[2]), # No. of colors in image (0 if >=8bpp) + 'reserved': i8(s[3]), + 'planes': i16(s[4:]), + 'bpp': i16(s[6:]), + 'size': i32(s[8:]), + 'offset': i32(s[12:]) + } + + # See Wikipedia + for j in ('width', 'height'): + if not icon_header[j]: + icon_header[j] = 256 + + # See Wikipedia notes about color depth. + # We need this just to differ images with equal sizes + icon_header['color_depth'] = (icon_header['bpp'] or + (icon_header['nb_color'] != 0 and + ceil(log(icon_header['nb_color'], + 2))) or 256) + + icon_header['dim'] = (icon_header['width'], icon_header['height']) + icon_header['square'] = (icon_header['width'] * + icon_header['height']) + + self.entries.append(icon_header) + + self.entries = sorted(self.entries, key=lambda x: x['color_depth']) + # ICO images are usually squares + # self.entries = sorted(self.entries, key=lambda x: x['width']) + self.entries = sorted(self.entries, key=lambda x: x['square']) + self.entries.reverse() + + def sizes(self): + """ + Get a list of all available icon sizes and color depths. + """ + return set((h['width'], h['height']) for h in self.entries) + + def getimage(self, size, bpp=False): + """ + Get an image from the icon + """ + for (i, h) in enumerate(self.entries): + if size == h['dim'] and (bpp is False or bpp == h['color_depth']): + return self.frame(i) + return self.frame(0) + + def get_images(self): + images = [] + for i, header in enumerate(self.entries): + f = self.frame(i) + if f.format: + images.append(self.frame(i)) + return images + + def frame(self, idx): + """ + Get an image from frame idx + """ + + header = self.entries[idx] + + self.buf.seek(header['offset']) + data = self.buf.read(8) + self.buf.seek(header['offset']) + + if data[:8] == PngImagePlugin._MAGIC: + # png frame + s = StringIO() + s.write(self.buf.read(header['size'])) + s.seek(0) + self.buf.seek(header['offset']) + im = Image.open(s) + else: + # XOR + AND mask bmp frame + s = StringIO() + s.write(self.buf.read(header['size'])) + s.seek(0) + self.buf.seek(header['offset']) + im = bmpimageplugin.DibImageFile(s) + #im = im.to_bitmapimage(header) + #print im.tile + # change tile dimension to only encompass XOR image + im.size = (im.size[0], int(im.size[1] / 2)) + #print im.size + d, e, o, a = im.tile[0] + im.tile[0] = d, (0, 0) + im.size, o, a + + # figure out where AND mask image starts + mode = a[0] + bpp = 8 + for k in BmpImagePlugin.BIT2MODE.keys(): + if mode == BmpImagePlugin.BIT2MODE[k][1]: + bpp = k + break + + if 32 == bpp: + # 32-bit color depth icon image allows semitransparent areas + # PIL's DIB format ignores transparency bits, recover them. + # The DIB is packed in BGRX byte order where X is the alpha + # channel. + + # Back up to start of bmp data + self.buf.seek(o) + # extract every 4th byte (eg. 3,7,11,15,...) + alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4] + + # convert to an 8bpp grayscale image + mask = Image.frombuffer( + 'L', # 8bpp + im.size, # (w, h) + alpha_bytes, # source chars + 'raw', # raw decoder + ('L', 0, -1) # 8bpp inverted, unpadded, reversed + ) + else: + # get AND image from end of bitmap + w = im.size[0] + if (w % 32) > 0: + # bitmap row data is aligned to word boundaries + w += 32 - (im.size[0] % 32) + + # the total mask data is + # padded row size * height / bits per char + + and_mask_offset = o + int(im.size[0] * im.size[1] * + (bpp / 8.0)) + total_bytes = int((w * im.size[1]) / 8) + + self.buf.seek(and_mask_offset) + maskData = self.buf.read(total_bytes) + + # convert raw data to image + mask = Image.frombuffer( + '1', # 1 bpp + im.size, # (w, h) + maskData, # source chars + 'raw', # raw decoder + ('1;I', int(w/8), -1) # 1bpp inverted, padded, reversed + ) + + # now we have two images, im is XOR image and mask is AND image + + if im.mode != 'RGBA': + im = im.convert('RGBA') + im.putalpha(mask) + + return im + + +## +# Image plugin for Windows Icon files. + +class IcoImageFile(ImageFile.ImageFile): + """ + PIL read-only image support for Microsoft Windows .ico files. + + By default the largest resolution image in the file will be loaded. This + can be changed by altering the 'size' attribute before calling 'load'. + + The info dictionary has a key 'sizes' that is a list of the sizes available + in the icon file. + + Handles classic, XP and Vista icon formats. + + This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis + . + https://code.google.com/p/casadebender/wiki/Win32IconImagePlugin + """ + format = "ICO" + format_description = "Windows Icon" + + def _open(self): + self.ico = IcoFile(self.fp) + self.info['sizes'] = self.ico.sizes() + self.size = self.ico.entries[0]['dim'] + self.load() + + def load(self): + im = self.ico.getimage(self.size) + # if tile is PNG, it won't really be loaded yet + im.load() + self.im = im.im + self.mode = im.mode + self.size = im.size + + def get_images(self): + return self.ico.get_images() + + def load_seek(self): + # Flage the ImageFile.Parser so that it + # just does all the decode at the end. + pass + +SAVE = { + "1": ("1", 1, 2), + "L": ("L", 8, 256), + "P": ("P", 8, 256), + "RGB": ("BGR", 24, 0), + "RGBA": ("BGRA", 32, 0), +} + +def get_data(im): + s = StringIO() + if im.format != 'DIB': + im.save(s, im.format) + else: + s.write(im.buf) + s.seek(0) + + if im.format == 'BMP': + bmp_f = s + bmp_f.seek(10) + offset = i32(bmp_f.read(4)) + dib_size = i32(bmp_f.read(4)) + dib = o32(dib_size)+bytearray(bmp_f.read(36)) + dib[:4] = o32(40) + dib[8:12] = o32(i32(str(dib[8:12]))*2) + dib[16:20] = o32(0) + dib = dib[:40] + bmp_f.seek(offset) + data = bytearray(bmp_f.read()) + data = dib+data[24:] + bytearray(24) + else: + data = bytearray(s.read()) + + return data + + + +def _save(image, fp, filename, check=0): + data = None + images = [] + if image.format != 'ICO': + bmp_f = StringIO() + image.save(bmp_f, format='BMP') + bmp_f.seek(0) + im2 = Image.open(bmp_f) + if im2.format: + images.append(im2) + else: + images = image.get_images() + + number_of_images = len(images) + + header_size = 6+16*number_of_images + + # ico header + fp.write(o16(0) + # reserved + o16(1) + # file type + o16(number_of_images)) # number of images + + current_offset = header_size + for i, im in enumerate(images): + + data = get_data(im) + + try: + rawmode, bits, colors = SAVE[im.mode] + except KeyError: + raise IOError("cannot write mode %s as BMP" % im.mode) + + fp.write(o8(im.size[0]) + # width + o8(im.size[1]) + # height + o8(0) + # color palette + o8(0) + # reserved + o16(1) + # planes + o16(bits) + # depth + o32(len(data)) + # size of image in bytes + o32(current_offset) # offset + + ) + current_offset += len(data) + + for im in images: + + data = get_data(im) + fp.write(data) + + fp.flush() + + +# +# -------------------------------------------------------------------- + +Image.register_open(IcoFile.format, IcoImageFile, _accept) +Image.register_save(IcoFile.format, _save) + +Image.register_extension(IcoFile.format, '.ico') diff --git a/pe.py b/pe.py index e10faf5..da942a3 100644 --- a/pe.py +++ b/pe.py @@ -1,10 +1,24 @@ import os import struct -try: - import PythonMagick as pm -except ImportError: - print 'PythonMagick is required to replace pe icon file. This functionality is disabled because you need to install it.' - pm = None +from cStringIO import StringIO + +from ico_plugin import * + +def resize(image, size): + output = StringIO() + back = Image.new('RGBA', size, (0,0,0,0)) + image.thumbnail(size, Image.ANTIALIAS) + offset = [0,0] + if image.size[0] >= image.size[1]: + offset[1] = back.size[1]/2-image.size[1]/2 + else: + offset[0] = back.size[0]/2-image.size[0]/2 + back.paste(image, tuple(offset)) + back.save(output, image.format) + contents = output.getvalue() + output.close() + return contents + struct_symbols = {1:'B',#byte 2:'H',#word @@ -1224,9 +1238,6 @@ class PEFile(Printable): """ icon_path = str(os.path.expanduser(icon_path)) #this needs to be a string and not unicode - if pm is None: - raise Exception('PythonMagick is required to run this function.') - if not os.path.exists(icon_path): raise Exception('Icon {} does not exist'.format(icon_path)) @@ -1240,13 +1251,21 @@ class PEFile(Printable): group_header = GroupHeader.parse_from_data(self.pe_file_data, absolute_offset=g_icon_data_entry.get_data_absolute_offset()) g_entry = group_header.entries[0] - icon = pm.Image(icon_path) - icon.sample('!{}x{}'.format(g_entry.Width.value, g_entry.Height.value)) #resize. Force aspect ratio to change with ! - icon.magick('ICO') # convert to ico - b = pm.Blob() - icon.write(b) + icon = Image.open(icon_path) + i_data = resize(icon, (g_entry.Width.value, g_entry.Height.value)) + s = StringIO() + s.write(i_data) + s.seek(0) - icon_data = bytearray(b.data) + icon = Image.open(s) + s2 = StringIO() + icon.save(s2, 'ico') + new_icon_size = s2.tell() + s2.seek(0) + icon_file_size = g_entry.DataSize.value+group_header.size+g_entry.size+2 + + #9662 is the exact length of the icon in nw.exe + icon_data = bytearray(s2.read()) + bytearray(icon_file_size-new_icon_size) icon_header = IconHeader.parse_from_data(icon_data, absolute_offset=0)