Snippets

grimhacker png2ico.py

Updated by grimhacker

File png2ico.py Modified

  • Ignore whitespace
  • Hide word diff
     with this program; if not, write to the Free Software Foundation, Inc.,
     51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 '''
+# Adapted from https://github.com/aclements/commuter/blob/master/web/png2ico
+# and https://code.google.com/p/reply-manager/source/browse/build/png2ico.py
 
 __version__ = "$Revision: 1.0 $"
 # $Source$
 
 import os
 import struct
+import ctypes
 import logging
 import argparse
 
 from io import BytesIO
 
 
+# ref: http://msdn.microsoft.com/en-us/library/ms997538
+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."""
+    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:
         return self._sizes
 
     def _resize(self, infile, size):
-        """resize image to size x size.
-        returns file like object."""
+        """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)
+        #outfile = "{0}_{1}.png".format(os.path.split(infile)[1], 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)
+            im = Image.open(infile).convert("RGBA")         # open and convert to RGBA
+            im.thumbnail((size, size), Image.ANTIALIAS)     # Create thumbnail of designated size
+            im.save(outfile, "PNG")                         # Save thumbnail to BytesIO instance
+            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)  # Give the BytesIO object a name
         except Exception as e:
             raise Exception("Cannot resize '{0}' to {1}x{1} {2}".format(infile, size, e))
         else:
+            #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"""
-        # Adapted from https://github.com/aclements/commuter/blob/master/web/png2ico
+    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 = image_file.read()
+            # 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(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:
+            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(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
+        self.log.debug("Building icon...")
+        header_section = []
+        data_section = []
 
-        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)
+        # Set up the icon header
+        icondir = ICONDIR()
+        icondir.idReserved = 0
+        icondir.idType = 1  # Icons are type 1
+        icondir.idCount = len(images)
+        header_section.append(icondir)
+        
+        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
+            header_section.append(iconentry)
+            data_section.append(data)
 
         self.log.debug("Writing icon...")
         with open(out_file, "wb") as f:
-            f.write(hdata + idata)
+            # Write header sections followed by data sections
+            for section in header_section + data_section:
+                f.write(section)
 
     def run(self):
         """"""
                 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)
+        #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.
+    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__)
                     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 = 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)
Created by grimhacker

File png2ico.py Added

  • Ignore whitespace
  • Hide word diff
+'''
+.       .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))