Pillow/src/PIL/IcoImagePlugin.py

340 lines
11 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 i16le as i16
from ._binary import i32le as i32
2021-05-26 23:21:28 +03:00
from ._binary import o32le as o32
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
2021-05-26 23:21:28 +03:00
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
provided_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)
tmp = provided_images.get(size)
if not tmp:
# TODO: invent a more convenient method for proportional scalings
tmp = im.copy()
tmp.thumbnail(size, Image.LANCZOS, reducing_gap=None)
2021-05-26 23:21:28 +03:00
bits = BmpImagePlugin.SAVE[tmp.mode][1] if bmp else 32
fp.write(struct.pack("<H", bits)) # wBitCount(2)
image_io = BytesIO()
if bmp:
tmp.save(image_io, "dib")
if bits != 32:
and_mask = Image.new("1", tmp.size)
ImageFile._save(
and_mask, image_io, [("raw", (0, 0) + tmp.size, 0, ("1", 0, -1))]
)
else:
tmp.save(image_io, "png")
2014-11-07 22:01:46 +03:00
image_io.seek(0)
image_bytes = image_io.read()
2021-05-26 23:21:28 +03:00
if bmp:
image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
2014-11-07 22:01:46 +03:00
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)
2013-06-05 21:01:05 +04:00
# 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 = {
"width": s[0],
"height": s[1],
"nb_color": s[2], # No. of colors in image (0 if >=8bpp)
"reserved": 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)
Image._decompression_bomb_check(im.size)
2013-06-05 21:01:05 +04:00
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
2021-04-22 14:18:21 +03:00
bpp = header["bpp"]
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
total_bytes = int((w * im.size[1]) / 8)
2021-08-10 00:04:36 +03:00
and_mask_offset = header["offset"] + header["size"] - total_bytes
2013-06-05 21:01:05 +04:00
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. If you are unable to view the icon in Windows, convert the
image to "RGBA" mode before saving.
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 Image.Image.load(self)
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")