Snippets

grimhacker png2ico.py

You are viewing an old version of this snippet. View the current version.
Revised by grimhacker c7bb39e
'''
.       .1111...          | Title: png2ico.py
    .10000000000011.   .. | Author: Oliver Morton (Sec-1 Ltd)
 .00              000...  | Email: oliverm-tools@sec-1.com
1                  01..   | Description:
                    ..    | Create a multisize ICO from a PNG file.
                   ..     | Requires:
GrimHacker        ..      | pillow
                 ..       |
grimhacker.com  ..        |
@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
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    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.
'''

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

import os
import struct
import logging
import argparse

from PIL import Image
from io import BytesIO


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
        else:
            raise Exception("PNG image_files required")
        if out_file:
            self._out_file = out_file
        else:
            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
        else:
            raise Exception("list of integer sizes required")

    @property
    def image_files(self):
        return self._image_files

    @property
    def out_file(self):
        return self._out_file

    @property
    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 = StringIO()
        #outfile = "{0}_{1}.png".format(os.path.splitext(infile)[0], size)
        try:
            im = Image.open(infile)
            im.thumbnail((size, size), Image.ANTIALIAS)
            im.save(outfile, "PNG")
            outfile.seek(0)  # Rewind the BytesIO object to the start ready for reading.
            outfile.name = "{0}_{1}.png".format(os.path.split(infile)[1], size)
        except Exception as e:
            raise Exception("Cannot resize '{0}' to {1}x{1} {2}".format(infile, size, e))
        else:
            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"""
        # Adapted from https://github.com/aclements/commuter/blob/master/web/png2ico
        self.log.debug("Loading image files...")
        images = []
        for image_file in image_files:
            data = image_file.read()
            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(image_file.name))
            else:
                self.log.debug("{0} is a PNG image".format(image_file.name))
            w, h, bpp = struct.unpack('!IIB', data[16:16+9])
            if w > 256 or h > 256:
                raise Exception("{0} exceeds 256x256 size limit!".format(image_file.name))
            else:
                self.log.debug("{0} is within 256x256 size limit.".format(image_file.name))
            images.append((data, w, h, bpp))

        self.log.debug("Building icon data...")
        hdata = struct.pack('<HHH', 0, 1, len(images))
        idata = bytearray()
        pos = len(hdata) + len(images) * 16

        for data, w, h, bpp in images:
            if w == 256:
                w = 0
            if h == 256:
                h = 0
            hdata += struct.pack('<BBBBHHII', w, h, 0, 0, 1, bpp, len(data), pos)
            pos += len(data)
            idata.extend(data)

        self.log.debug("Writing icon...")
        with open(out_file, "wb") as f:
            f.write(hdata + idata)

    def run(self):
        """"""
        images = []
        if len(self.image_files) > 1:
            self.log.info("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(f.read())
                    image.name = image_file
                    image.seek(0)
                    images.append(image)
        else:
            self.log.info("Creating thumbnails...")
            for size in self.sizes:
                images.append(self._resize(self.image_files[0], size))
        self.log.info("Creating ICO file with {0} images...".format(len(images)))
        self._create_ico(self.out_file, images)


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.
""".format(__version__)

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

    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",
               formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    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:
        print_version()
        exit()

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

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

You can clone a snippet to your computer for local editing. Learn more.