Added custom icon support so PIL replaces ImageMagick.

This commit is contained in:
Joey Payne 2014-11-02 22:54:38 +13:00
commit 0cb12bd2f5
3 changed files with 720 additions and 14 deletions

327
bmpimageplugin.py Normal file
View file

@ -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")

360
ico_plugin.py Normal file
View file

@ -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
# <casadebender@gmail.com>.
# 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
<casadebender@gmail.com>.
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')

47
pe.py
View file

@ -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)