Pillow/src/PIL/IcoImagePlugin.py

329 lines
10 KiB
Python
Raw Normal View History

2010-07-31 06:52:47 +04:00
#
# 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.
#
2014-08-26 17:47:10 +04:00
# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
# <casadebender@gmail.com>.
2016-02-22 10:38:04 +03:00
# https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
2013-06-05 21:01:05 +04:00
#
# Icon format references:
2015-12-28 16:04:39 +03:00
# * https://en.wikipedia.org/wiki/ICO_(file_format)
2016-02-10 11:37:16 +03:00
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
2010-07-31 06:52:47 +04:00
2014-11-07 22:01:46 +03:00
import struct
2019-05-12 13:44:29 +03:00
import warnings
2014-11-08 09:49:50 +03:00
from io import BytesIO
from math import ceil, log
2014-11-07 22:01:46 +03:00
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
from ._binary import i8, i16le as i16, i32le as i32
2010-07-31 06:52:47 +04:00
#
# --------------------------------------------------------------------
2013-06-05 21:01:05 +04:00
_MAGIC = b"\0\0\1\0"
2010-07-31 06:52:47 +04:00
2014-08-26 17:47:10 +04:00
2014-11-07 22:01:46 +03:00
def _save(im, fp, filename):
fp.write(_MAGIC) # (2+2)
2019-03-21 16:28:20 +03:00
sizes = im.encoderinfo.get(
"sizes",
[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
)
2014-11-07 22:01:46 +03:00
width, height = im.size
2019-03-21 16:28:20 +03:00
sizes = filter(
lambda x: False
if (x[0] > width or x[1] > height or x[0] > 256 or x[1] > 256)
else True,
sizes,
)
2016-11-30 17:50:44 +03:00
sizes = list(sizes)
2015-05-13 13:05:45 +03:00
fp.write(struct.pack("<H", len(sizes))) # idCount(2)
2019-03-21 16:28:20 +03:00
offset = fp.tell() + len(sizes) * 16
2020-04-19 16:54:53 +03:00
alt_images = {im.size: im for im in im.encoderinfo.get("append_images", [])}
2014-11-07 22:01:46 +03:00
for size in sizes:
width, height = size
2016-11-30 15:50:53 +03:00
# 0 means 256
fp.write(struct.pack("B", width if width < 256 else 0)) # bWidth(1)
fp.write(struct.pack("B", height if height < 256 else 0)) # bHeight(1)
2014-11-07 22:01:46 +03:00
fp.write(b"\0") # bColorCount(1)
fp.write(b"\0") # bReserved(1)
fp.write(b"\0\0") # wPlanes(2)
2015-05-13 13:05:45 +03:00
fp.write(struct.pack("<H", 32)) # wBitCount(2)
2014-11-07 22:01:46 +03:00
image_io = BytesIO()
if size in alt_images:
tmp = alt_images[size]
else:
# TODO: invent a more convenient method for proportional scalings
tmp = im.copy()
tmp.thumbnail(size, Image.LANCZOS, reducing_gap=None)
2014-11-08 09:49:50 +03:00
tmp.save(image_io, "png")
2014-11-07 22:01:46 +03:00
image_io.seek(0)
image_bytes = image_io.read()
bytes_len = len(image_bytes)
2015-05-13 13:05:45 +03:00
fp.write(struct.pack("<I", bytes_len)) # dwBytesInRes(4)
fp.write(struct.pack("<I", offset)) # dwImageOffset(4)
2014-11-07 22:01:46 +03:00
current = fp.tell()
fp.seek(offset)
fp.write(image_bytes)
offset = offset + bytes_len
fp.seek(current)
2010-07-31 06:52:47 +04:00
def _accept(prefix):
2013-06-05 21:01:05 +04:00
return prefix[:4] == _MAGIC
2010-07-31 06:52:47 +04:00
class IcoFile:
2013-06-05 21:01:05 +04:00
def __init__(self, buf):
"""
Parse image from file-like object containing ico file data
"""
2013-06-05 21:01:05 +04:00
# check magic
s = buf.read(6)
if not _accept(s):
raise SyntaxError("not an ICO file")
2013-06-05 21:01:05 +04:00
self.buf = buf
self.entry = []
# Number of items in file
self.nb_items = i16(s[4:])
# Get headers for each item
for i in range(self.nb_items):
s = buf.read(16)
2013-06-05 21:01:05 +04:00
icon_header = {
2019-03-21 16:28:20 +03:00
"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:]),
2013-06-05 21:01:05 +04:00
}
2013-06-05 21:01:05 +04:00
# See Wikipedia
2019-03-21 16:28:20 +03:00
for j in ("width", "height"):
2013-06-05 21:01:05 +04:00
if not icon_header[j]:
icon_header[j] = 256
2013-06-05 21:01:05 +04:00
# See Wikipedia notes about color depth.
# We need this just to differ images with equal sizes
2019-03-21 16:28:20 +03:00
icon_header["color_depth"] = (
icon_header["bpp"]
or (
icon_header["nb_color"] != 0
and ceil(log(icon_header["nb_color"], 2))
)
or 256
)
2019-03-21 16:28:20 +03:00
icon_header["dim"] = (icon_header["width"], icon_header["height"])
icon_header["square"] = icon_header["width"] * icon_header["height"]
2013-06-05 21:01:05 +04:00
self.entry.append(icon_header)
2019-03-21 16:28:20 +03:00
self.entry = sorted(self.entry, key=lambda x: x["color_depth"])
2013-06-05 21:01:05 +04:00
# ICO images are usually squares
# self.entry = sorted(self.entry, key=lambda x: x['width'])
2019-03-21 16:28:20 +03:00
self.entry = sorted(self.entry, key=lambda x: x["square"])
2013-06-05 21:01:05 +04:00
self.entry.reverse()
2013-06-05 21:01:05 +04:00
def sizes(self):
"""
Get a list of all available icon sizes and color depths.
"""
2019-03-21 16:28:20 +03:00
return {(h["width"], h["height"]) for h in self.entry}
2019-05-12 13:44:29 +03:00
def getentryindex(self, size, bpp=False):
for (i, h) in enumerate(self.entry):
2019-06-11 11:42:05 +03:00
if size == h["dim"] and (bpp is False or bpp == h["color_depth"]):
2019-05-12 13:44:29 +03:00
return i
return 0
2013-06-05 21:01:05 +04:00
def getimage(self, size, bpp=False):
"""
Get an image from the icon
"""
2019-05-12 13:44:29 +03:00
return self.frame(self.getentryindex(size, bpp))
2013-06-05 21:01:05 +04:00
def frame(self, idx):
"""
Get an image from frame idx
"""
2013-06-05 21:01:05 +04:00
header = self.entry[idx]
2019-03-21 16:28:20 +03:00
self.buf.seek(header["offset"])
2013-06-05 21:01:05 +04:00
data = self.buf.read(8)
2019-03-21 16:28:20 +03:00
self.buf.seek(header["offset"])
2013-06-05 21:01:05 +04:00
if data[:8] == PngImagePlugin._MAGIC:
# png frame
im = PngImagePlugin.PngImageFile(self.buf)
else:
# XOR + AND mask bmp frame
im = BmpImagePlugin.DibImageFile(self.buf)
2019-09-29 07:14:38 +03:00
Image._decompression_bomb_check(im.size)
2013-06-05 21:01:05 +04:00
# change tile dimension to only encompass XOR image
im._size = (im.size[0], int(im.size[1] / 2))
2013-06-05 21:01:05 +04:00
d, e, o, a = im.tile[0]
2014-08-26 17:47:10 +04:00
im.tile[0] = d, (0, 0) + im.size, o, a
2013-06-05 21:01:05 +04:00
# figure out where AND mask image starts
mode = a[0]
bpp = 8
for k, v in BmpImagePlugin.BIT2MODE.items():
if mode == v[1]:
2013-06-05 21:01:05 +04:00
bpp = k
break
2013-06-05 21:01:05 +04:00
if 32 == bpp:
# 32-bit color depth icon image allows semitransparent areas
2014-08-26 17:47:10 +04:00
# PIL's DIB format ignores transparency bits, recover them.
# The DIB is packed in BGRX byte order where X is the alpha
# channel.
2013-06-05 21:01:05 +04:00
# 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(
2019-03-21 16:28:20 +03:00
"L", # 8bpp
im.size, # (w, h)
alpha_bytes, # source chars
"raw", # raw decoder
("L", 0, -1), # 8bpp inverted, unpadded, reversed
2013-06-05 21:01:05 +04:00
)
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)
2014-08-26 17:47:10 +04:00
# the total mask data is
# padded row size * height / bits per char
2013-06-05 21:01:05 +04:00
2019-03-21 16:28:20 +03:00
and_mask_offset = o + int(im.size[0] * im.size[1] * (bpp / 8.0))
2013-06-05 21:01:05 +04:00
total_bytes = int((w * im.size[1]) / 8)
self.buf.seek(and_mask_offset)
2016-11-30 15:51:30 +03:00
mask_data = self.buf.read(total_bytes)
2013-06-05 21:01:05 +04:00
# convert raw data to image
mask = Image.frombuffer(
2019-03-21 16:28:20 +03:00
"1", # 1 bpp
im.size, # (w, h)
mask_data, # source chars
"raw", # raw decoder
("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed
2013-06-05 21:01:05 +04:00
)
2013-06-05 21:01:05 +04:00
# now we have two images, im is XOR image and mask is AND image
# apply mask image as alpha channel
2019-03-21 16:28:20 +03:00
im = im.convert("RGBA")
2013-06-05 21:01:05 +04:00
im.putalpha(mask)
2013-06-05 21:01:05 +04:00
return im
2014-08-26 17:47:10 +04:00
2010-07-31 06:52:47 +04:00
##
# Image plugin for Windows Icon files.
2019-03-21 16:28:20 +03:00
2013-06-05 21:01:05 +04:00
class IcoImageFile(ImageFile.ImageFile):
"""
PIL read-only image support for Microsoft Windows .ico files.
2014-08-26 17:47:10 +04:00
By default the largest resolution image in the file will be loaded. This
can be changed by altering the 'size' attribute before calling 'load'.
2010-07-31 06:52:47 +04:00
The info dictionary has a key 'sizes' that is a list of the sizes available
2013-06-05 21:01:05 +04:00
in the icon file.
Handles classic, XP and Vista icon formats.
When saving, PNG compression is used. Support for this was only added in
Windows Vista.
2014-08-26 17:47:10 +04:00
This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
<casadebender@gmail.com>.
2016-02-22 10:38:04 +03:00
https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
2013-06-05 21:01:05 +04:00
"""
2019-03-21 16:28:20 +03:00
2010-07-31 06:52:47 +04:00
format = "ICO"
format_description = "Windows Icon"
def _open(self):
2013-06-05 21:01:05 +04:00
self.ico = IcoFile(self.fp)
2019-03-21 16:28:20 +03:00
self.info["sizes"] = self.ico.sizes()
self.size = self.ico.entry[0]["dim"]
2013-06-05 21:01:05 +04:00
self.load()
@property
def size(self):
return self._size
@size.setter
def size(self, value):
2019-03-21 16:28:20 +03:00
if value not in self.info["sizes"]:
raise ValueError("This is not one of the allowed sizes of this image")
self._size = value
2013-06-05 21:01:05 +04:00
def load(self):
2019-05-29 14:05:45 +03:00
if self.im and self.im.size == self.size:
# Already loaded
return
2013-06-05 21:01:05 +04:00
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
2019-05-12 13:44:29 +03:00
if im.size != self.size:
warnings.warn("Image was not the expected size")
index = self.ico.getentryindex(self.size)
2019-06-11 11:42:05 +03:00
sizes = list(self.info["sizes"])
2019-05-12 13:44:29 +03:00
sizes[index] = im.size
2019-06-11 11:42:05 +03:00
self.info["sizes"] = set(sizes)
2019-05-12 13:44:29 +03:00
self.size = im.size
2010-07-31 06:52:47 +04:00
def load_seek(self):
2015-01-29 07:23:15 +03:00
# Flag the ImageFile.Parser so that it
2014-08-26 17:47:10 +04:00
# just does all the decode at the end.
pass
2019-03-21 16:28:20 +03:00
2010-07-31 06:52:47 +04:00
#
# --------------------------------------------------------------------
2016-11-30 15:51:30 +03:00
Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
Image.register_save(IcoImageFile.format, _save)
Image.register_extension(IcoImageFile.format, ".ico")
Image.register_mime(IcoImageFile.format, "image/x-icon")