Created by grimhacker last modified
.       .1111...          | Title:
    .10000000000011.   .. | Author: Oliver Morton (Sec-1 Ltd)
 .00              000...  | Email:
1                  01..   | Description:
                    ..    | Create a multisize ICO from a PNG file.
                   ..     | Requires:
GrimHacker        ..      | pillow
                 ..       |  ..        |
@grimhacker    ..         |
PNG2ICO - Create a multisize ICO from a PNG
    Copyright (C) 2015  Oliver Morton (Sec-1 Ltd)
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
# Adapted from
# and

__version__ = "$Revision: 1.0 $"
# $Source$

import os
import struct
import ctypes
import logging
import argparse

from PIL import Image
from io import BytesIO

# ref:
class ICONDIR(ctypes.LittleEndianStructure):
    _pack_ = 1
    _fields_ = [("idReserved", ctypes.c_ushort),
                ("idType", ctypes.c_ushort),
                ("idCount", ctypes.c_ushort)]

class ICONDIRENTRY(ctypes.LittleEndianStructure):
    _pack_ = 1
    _fields_ = [("bWidth", ctypes.c_byte),
                ("bHeight", ctypes.c_byte),
                ("bColorCount", ctypes.c_byte),
                ("bReserved", ctypes.c_byte),
                ("wPlanes", ctypes.c_ushort),
                ("wBitCount", ctypes.c_ushort),
                ("dwBytesInRes", ctypes.c_ulong),
                ("dwImageOffset", ctypes.c_ulong)]

class PNG2ICO(object):
    """PNG2ICO - Create a multisize ICO from a PNG
    First creates all required sizes of supplied image in memory then
    builds ICO file structure using struct and writes to specified
    output file."""
    def __init__(self, image_files, out_file, sizes=[256,48,32,16]):
        self.log = logging.getLogger(__name__)
        if image_files:
            self._image_files = image_files
            raise Exception("PNG image_files required")
        if out_file:
            self._out_file = out_file
            raise Exception("out_file required")
        if sizes and isinstance(sizes, list) and all(isinstance(item, int) for item in sizes):
            self._sizes = sorted(sizes, reverse=True)  # need largest first for ico format
            raise Exception("list of integer sizes required")

    def image_files(self):
        return self._image_files

    def out_file(self):
        return self._out_file

    def sizes(self):
        return self._sizes

    def _resize(self, infile, size):
        """Resize image to size x size.
        Returns file like object."""
        self.log.debug("Resizing {file_} to {size} x {size}".format(file_=infile, size=size))
        outfile = BytesIO()
        #outfile = "{0}_{1}.png".format(os.path.split(infile)[1], size)
            im ="RGBA")         # open and convert to RGBA
            im.thumbnail((size, size), Image.ANTIALIAS)     # Create thumbnail of designated size
  , "PNG")                         # Save thumbnail to BytesIO instance
                                   # Rewind the BytesIO object to the start ready for reading.
   = "{0}_{1}.png".format(os.path.split(infile)[1], size)  # Give the BytesIO object a name
        except Exception as e:
            raise Exception("Cannot resize '{0}' to {1}x{1} {2}".format(infile, size, e))
            #return open(outfile, "rb")
            return outfile

    def _create_ico(self, out_file, image_files):        
        """Create ico file from list of image file like objects.
        This builds the ico file using struct based on the specification."""
        self.log.debug("Loading image files...")
        images = []
        for image_file in image_files:
            data =
            # Check image_file is a PNG
            if not (data.startswith('\x89PNG\x0d\x0a\x1a\x0a') and
                data[12:].startswith('IHDR') and
                len(data) >= 29):
                    raise Exception('not a valid png file: {0}'.format(
                self.log.debug("{0} is a PNG image".format(
            w, h, bpp = struct.unpack('!IIB', data[16:16+9])  # Extract width, height, and bits per pixel
            if w > 256 or h > 256:  # Check maximum icon size is not exceeded
                raise Exception("{0} exceeds 256x256 size limit!".format(
                self.log.debug("{0} is within 256x256 size limit.".format(
            images.append((data, w, h, bpp))

        self.log.debug("Building icon...")
        header_section = []
        data_section = []

        # Set up the icon header
        icondir = ICONDIR()
        icondir.idReserved = 0
        icondir.idType = 1  # Icons are type 1
        icondir.idCount = len(images)
        icondir_size = ctypes.sizeof(icondir)
        iconentry_size = len(images) * ctypes.sizeof(ICONDIRENTRY())
        offset = icondir_size + iconentry_size  # calculate offset after entire header section
        for data, width, height, bpp in images:
            if width == 256:
                width = 0
            if height == 256:
                height = 0
            iconentry = ICONDIRENTRY()
            iconentry.bWidth = width
            iconentry.bHeight = height
            iconentry.bColorCount = 0           # Truecolor images have color count set to 0
            iconentry.bReserved = 0             # Reserved, must be 0
            iconentry.wPlanes = 1               # PNGs have 1 color plane
            iconentry.wBitCount = bpp           # bits per pixel from image_file
            iconentry.dwBytesInRes = len(data)  # Length of the image data
            # The data will be right after the icondir and iconentry
            iconentry.dwImageOffset = offset    # Offset of header and data so far
            offset += len(data)                 # Calculate new offset

        self.log.debug("Writing icon...")
        with open(out_file, "wb") as f:
            # Write header sections followed by data sections
            for section in header_section + data_section:

    def run(self):
        images = []
        if len(self.image_files) > 1:
  "Assuming thumbnails have been provided in largest to smallest order, opening...")
            for image_file in self.image_files:
                with open(image_file,"rb") as f:
                    image = BytesIO(
           = image_file
  "Creating thumbnails...")
            for size in self.sizes:
                images.append(self._resize(self.image_files[0], size))"Creating ICO file with {0} images...".format(len(images)))
        self._create_ico(self.out_file, images)
        #for image in images:
        #    image.close()

if __name__ == "__main__":
    print """
    PNG2ICO - Create a multisize ICO from a PNG
    Copyright (C) 2015  Oliver Morton (Sec-1 Ltd)    
    This program comes with ABSOLUTELY NO WARRANTY.
    This is free software, and you are welcome to redistribute it
    under certain conditions. See GPLv2 License.

    def print_version():
        """Print command line version banner."""
        print """
.       .1111...          | Title: {0}    
    .10000000000011.   .. | Author: Oliver Morton (Sec-1 Ltd)
 .00              000...  | Email:
1                  01..   | Description:
                    ..    | Create a multisize ICO from a PNG file.
                   ..     |
GrimHacker        ..      |
                 ..       |  ..        |
@grimhacker    ..         |

    class SizesAction(argparse.Action):
        def __call__(self, parser, namespace, values, option_string=None):
            # require values to be a list of integers between 1 and 256 (inclusive)
            max = 256
            min = 1
            for value in values:
                if value > max or value < min:
                    parser.error("sizes must be between {0} and {1}".format(max, min))
            setattr(namespace, self.dest, values)

    parser = argparse.ArgumentParser(description="Create a multisize ICO from a PNG")
    parser.add_argument("-V", "--version", help="Print version banner", action="store_true")
    parser.add_argument("-v", "--verbose", help="Debug logging", action="store_true")
    parser.add_argument("-i", "--input_file", help="Input PNG file", nargs="+", required=True)
    parser.add_argument("-o", "--output_file", help="Output ico filename", default="favicon.ico")
    parser.add_argument("-s", "--sizes", help="image sizes to create in ico", type=int, nargs="+", default=[256,48,32,16], action=SizesAction)
    args = parser.parse_args()

    if args.version:

    if args.verbose:
        level = logging.DEBUG
        level = logging.INFO
                        format="%(levelname)s: %(message)s")

        png2ico = PNG2ICO(image_files=args.input_file, out_file=args.output_file, sizes=args.sizes)
    except Exception as e:
        logging.critical("Error running PNG2ICO: {0}".format(e))
    else:"Done. Check {0}".format(args.output_file))

Comments (0)