<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">"""
Script to generate CUGL icons

Icon generation is always annoying, because each platform has its own specific format.
This script automates this process by generating the standard formats for macOS, iOS,
Android and Windows. 

To work it must be given a 1024x1024 PNG file for the icon. The icon may have transparency,
but this transparency will be removed on all mobile platforms (where it is not allowed).
On those platforms the image will be composited on top of a background color. By default
this background color is white, but it can be specified as a second argument using a web
color string (WITHOUT THE #). For example, the default CUGL icon is generated with
    
    python iconify.py cugl.png ee3c30
    
The --transparent flag will only apply the backing color on mobile platforms.  Otherwise
it will be applied on both desktop and mobile platforms.

The --rounded flag will apply rounded corners on the desktop icons.  It is incompatible
with (and overridden by) --transparent.

Author: Walker White
Date: February 22, 2022
"""
from PIL import Image
import traceback
import os, os.path
import shutil
import math
import json
import argparse


#mark COLOR FUNCTIONS 

def rgb_to_web(rgb):
    """
    Returns a web color equivalent to the given rgb tuple
    
    Web colors are 7 characters starting with a # and followed by a six character
    hexadecimal string (2 characters per channel).
    
    Parameter rgb: The rgb color value
    Precondition: rgb is a 3-element tuple of ints 0..255
    """
    r = hex(rgb[0]).lower()[-2:]
    g = hex(rgb[1]).lower()[-2:]
    b = hex(rgb[2]).lower()[-2:]
    return "#"+r+g+b


def web_to_rgb(web):
    """
    Returns an rgb tuple for the given web color.
    
    The web color is a string that may or may not start with an #. It should have
    6 hexadecimal digits (2 characters per channel). The value returned is a 3-element
    tuple of ints in the range 0..255.
    
    Parameter web: The web color
    Precondition: web is a string with a six-character hexadecimal value
    """
    try:
        if web[0] == '#':
            web = web[1:]
        if len(web) &lt; 6:
            web = web + ('f'*(6-len(web)))
        web = web.lower()
        r = int(web[0:2],16)
        g = int(web[2:4],16)
        b = int(web[4:6],16)
        return (r,g,b)
    except:
        print(traceback.format_exc()[:-1])
        return (255,255,255)

pass
#mark -
#mark IMAGE RESAMPLING

def rdrect_image(image):
    """
    Returns a copy of image with rounded corners
    
    The corners of the image will be erased to turn the image into a 
    rounded rectangle. This method is intended to be applied to an image
    with no transparency and can have unexpected results if the image has
    transparency.
    
    The radius of each of the rounded rectangle corners is 10% of the
    minium image dimension.
    
    Parameter image: The image to copy
    Precondition: image is a PIL Image object
    """
    radius = min(*image.size)/10
    data = image.getdata()
    result = []
    
    w = image.size[0]
    h = image.size[0]
    for y in range(h):
        for x in range(w):
            p = data[x+y*w]
            a = rdrect_alpha(x,y,w,h,radius)
            a = round(a*255)
            p = (p[0],p[1],p[2],a)
            result.append(p)
    copy = Image.new('RGBA',(w,h),0)
    copy.putdata(result)
    return copy


def rdrect_alpha(x,y,w,h,r):
    """
    Computes the alpha percentage at (x,y) in the rounded rectangle (w,h,r).
    
    This function is used to give antialiased edges to a rounded rectangle. The
    rectangle has origin (0,0) and size (w,h). Each corner has radius r.
    
    Parameter x: The x-coordinate to test
    Precondition: x is a number (int or float)
    
    Parameter y: The y-coordinate to test
    Precondition: y is a number (int or float)
    
    Parameter w: The rectangle width
    Precondition: w &gt; 0 is a number (int or float)
    
    Parameter h: The rectangle height
    Precondition: h &gt; 0 is a number (int or float)
    
    Parameter r: The corner radius
    Precondition: r &gt;= 0 is a number (int or float)
    """
    if (x &lt; 0 or x &gt; w):
        return 0
    elif (y &lt; 0 or y &gt; h): 
        return 0
    elif (x &lt;= r and y &lt;= r):
        far  = math.sqrt((x-r)*(x-r)+(y-r)*(y-r))
        near = math.sqrt((x+1-r)*(x+1-r)+(y+1-r)*(y+1-r))
    elif (x &lt; r and y &gt; h-r):
        far  = math.sqrt((x-r)*(x-r)+(y-h+r)*(y-h+r))
        near = math.sqrt((x+1-r)*(x+1-r)+(y-1-h+r)*(y-1-h+r))
    elif (x &gt; w-r and y &gt; h-r):
        far  = math.sqrt((x-w+r)*(x-w+r)+(y-h+r)*(y-h+r))
        near = math.sqrt((x-1-w+r)*(x-1-w+r)+(y-1-h+r)*(y-1-h+r))
    elif (x &gt; w-r and y &lt; r):
        far  = math.sqrt((x-w+r)*(x-w+r)+(y-r)*(y-r))
        near = math.sqrt((x-1-w+r)*(x-1-w+r)+(y+1-r)*(y+1-r))
    else:
        far  = 0
        near = 0
    
    if near &gt;= r:
        return 0
    elif far &lt;= r:
        return 1
    else: # near &lt; r &lt; far
        return (r-near)/math.sqrt(2)


def circle_image(image):
    """
    Returns a copy of image inscribed inside of a circle.
    
    The circle is the largest one the inscribes the image rectangle. Hence its diameter
    is the minimum of the image width and height.
    
    Parameter image: The image to copy
    Precondition: image is a PIL Image object
    """
    data = image.getdata()
    result = []
    
    w = image.size[0]
    h = image.size[0]
    for y in range(h):
        for x in range(w):
            p = data[x+y*w]
            a = circle_alpha(x,y,w,h)
            a = round(a*255)
            p = (p[0],p[1],p[2],a)
            result.append(p)
    copy = Image.new('RGBA',(w,h),0)
    copy.putdata(result)
    return copy


def circle_alpha(x,y,w,h):
    """
    Computes the alpha percentage at (x,y) in the circle inscribing the rectangle (w,h).
    
    This function is used to give antialiased edges to a circle. The circle has center
    (w/2,h/2) and its diameter is the minimum of the w and h.
    
    Parameter x: The x-coordinate to test
    Precondition: x is a number (int or float)
    
    Parameter y: The y-coordinate to test
    Precondition: y is a number (int or float)
    
    Parameter w: The bounding width
    Precondition: w &gt; 0 is a number (int or float)
    
    Parameter h: The bounding height
    Precondition: h &gt; 0 is a number (int or float)
    """
    c = (w/2,h/2)
    r = min(*c)
    far = math.sqrt((x-c[0])*(x-c[0])+(y-c[1])*(y-c[1]))
    
    if (x &lt; 0 or x &gt; w):
        return 0
    elif (y &lt; 0 or y &gt; h): 
        return 0
    elif (x &lt;= c[0] and y &lt;= c[1]):
        near = math.sqrt((x+1-c[0])*(x+1-c[0])+(y+1-c[1])*(y+1-c[1]))
    elif (x &lt;= c[0] and y &gt;= c[1]):
        near = math.sqrt((x+1-c[0])*(x+1-c[0])+(y-1-c[1])*(y-1-c[1]))
    elif (x &gt;= c[0] and y &gt;= c[1]):
        near = math.sqrt((x-1-c[0])*(x-1-c[0])+(y-1-c[1])*(y-1-c[1]))
    elif (x &gt;= c[0] and y &lt;= c[1]):
        near = math.sqrt((x-1-c[0])*(x-1-c[0])+(y+1-c[1])*(y+1-c[1]))
    else:
        far  = 0
        near = 0
    
    if near &gt;= r:
        return 0
    elif far &lt;= r:
        return 1
    else: # near &lt; r &lt; far
        return (r-near)/math.sqrt(2)


def blend_image(image,color):
    """
    Returns a copy of the image blended with the given background color.
    
    Parameter image: The image to copy
    Precondition: image is a PIL Image object
    """
    if image is None:
        return None
    
    data = image.getdata()
    result = []
    if image.mode == 'RGB':
        for pix in data:
            result.append((pix[0],pix[1],pix[2],255))
    else:
        for pix in data:
            if pix[3] != 255:
                a = pix[3]/255.0
                r = min(int(pix[0]*a+color[0]*(1-a)),255)
                g = min(int(pix[1]*a+color[1]*(1-a)),255)
                b = min(int(pix[2]*a+color[2]*(1-a)),255)
                pix = (r,g,b,255)
            result.append(pix)
    
    copy = Image.new('RGBA',image.size,0)
    copy.putdata(result)
    return copy


def float_image(image):
    """
    Returns a copy of the image resized for Android dynamic icons.
    
    This method requires that that image is be square.  It resizes the image to be
    twice the size of the highest resolution icon (for better downscaling), and adds
    the empty padding that dynamic icons require.
    
    Parameter image: The image to copy
    Precondition: image is a PIL Image object
    """
    dold = 640
    dnew = 864
    copy = image.resize((dold,dold))
    data = copy.getdata()
    next = Image.new('RGBA',(dnew,dnew),0)
    
    result = []
    for y in range(dnew):
        for x in range(dnew):
            diff = (dnew-dold)//2
            if x &lt; diff or y &lt; diff or x &gt;= diff+dold or y &gt;= diff+dold:
                p = (0,0,0,0)
            else:
                p = data[(x-diff)+(y-diff)*dold]
            result.append(p)
    
    next.putdata(result)
    return next


pass
#mark -
#mark ICON FACTORY

class IconMaker(object):
    """
    A class for generating application icons.
    
    The class currently supports macOS, iOS, Android, and Windows.
    
    It works by specifying a foreground image and a background color. If the foreground
    image is opaque, the color has no effect. But images with transparency require
    a background color on mobile devices.
    
    Attribute image: The foreground image
    Invariant: image is a PIL Image object, or None
    
    Attribute name: The original file name of the foreground image (without suffix)
    Invariant: name is a string, or None
    
    Attribute color: The background color
    Invariant: color 3-element tuple of ints 0..255
    
    Attribute transparent: Whether to preserve transparency on desktop icons
    Invariant: transparent is a bool
    
    Attribute rounded: Whether to round the corners on desktop icons (like mobile)
    Invariant: rounded is a bool
    """
    
    def __init__(self,file,color=None,transparent=False,rounded=False):
        """
        Initializes the icon maker with the given values
        
        If the background color is not specified, the maker will use white as the 
        background color.
        
        Attribute file: The file name of the foreground image
        Precondition: file is string referencing an image file
    
        Attribute color: The background color
        Precondition: color is None or a web color (DEFAULT None)
    
        Attribute transparent: Whether to preserve transparency on desktop icons 
        Precondition: transparent is a bool (DEFAULT False)
    
        Attribute rounded: Whether to round the corners on desktop icons (like mobile)
        Precondition: rounded is a bool (DEFAULT False)
        """
        self.acquire(file)
        self.color = web_to_rgb(color)
        self.transparent = transparent
        self.rounded = rounded
    
    def acquire(self,file):
        """
        Loads the file into a PIL image
        
        If this fails, attributes image and name will be None.
        
        Attribute file: The file name of the foreground image
        Precondition: file is string
        """
        try:
            self.image = Image.open(file)
            if not self.image.mode in ['RGBA','RGB']:
                print('Image has invalid format: '+self.image.mode)
                self.image = None
            elif self.image.size != (1024,1024):
                print('Image has invalid size: '+repr(self.image.size))
                self.image = None
            self.name = os.path.splitext(os.path.split(file)[1])[0]
        except:
            print(traceback.format_exc()[:-1])
            self.image = None
            self.name =  None

    def generate(self):
        """
        Generates the icons for all major platforms.
        
        The icons will be placed in a directory called `output`.
        """
        if self.image is None:
            print("Could not generate icons")
            return
    
        if (not os.path.isdir('output')):
            os.mkdir('output')
        self.gen_macos()
        self.gen_ios()
        self.gen_android()
        self.gen_windows()
    
    def gen_macos(self):
        """
        Generates the icons for macOS
        
        The icons will be placed in a subdirectory called `macOS`. The folder 
        `AppIcon.appiconset` should be copied to the Resources folder of your 
        XCode project.
        """
        print('Generating macOS icons')
        path = ['output','macos','AppIcon.appiconset']
        if (not os.path.isdir(os.path.join(*path[:-1]))):
            os.mkdir(os.path.join(*path[:-1]))
        if (not os.path.isdir(os.path.join(*path))):
            os.mkdir(os.path.join(*path))
        
        if self.transparent:
            small = self.image
            large = self.image
        elif self.rounded:
            small = blend_image(self.image,self.color)
            large = rdrect_image(small)
        else:
            small = blend_image(self.image,self.color)
            large = small
        
        size = ((16,1),(16,2),(32,1),(32,2),(128,1),(128,2),(256,1),(256,2),(512,1),(512,2))
        data = []
        for s in size:
            info = {}
            info['filename'] = self.name
            info['idiom'] = 'mac'
            info['size'] = str(s[0])+'x'+str(s[0])
            info['scale'] = str(s[1])+'x'
            if s[1] == 1:
                suffix = '_' +info['size'] +'.png'
            else:
                suffix = '_' +info['size'] +'@'+info['scale']+'.png'
            info['filename'] = info['filename']+suffix
            data.append(info)
        
            d = s[0]*s[1]
            if d &gt; 64:
                copy = large.resize((d,d))
            else:
                copy = small.resize((d,d))
            copy.save(os.path.join(*(path+[info['filename']])),'PNG')
    
        contents = {'images':data}
        contents['info'] = { 'version' : 1, 'author' : 'xcode' }
        with open(os.path.join(*(path+['Contents.json'])),'w') as file:
            file.write(json.dumps(contents,indent=2))
    
    def gen_ios(self):
        """
        Generates the icons for iOS
        
        The icons will be placed in a subdirectory called `iOS`. The folder 
        `AppIcon.appiconset` should be copied to the Resources folder of your 
        XCode project.
        """
        print('Generating iOS icons')
        # Make sure the directory exists first
        path = ['output','ios','AppIcon.appiconset']
        if (not os.path.isdir(os.path.join(*path[:-1]))):
            os.mkdir(os.path.join(*path[:-1]))
        if (not os.path.isdir(os.path.join(*path))):
            os.mkdir(os.path.join(*path))
    
        format = {}
        format['iphone'] = ((20,2),(20,3),(29,2),(29,3),(40,2),(40,3),(60,2),(60,3))
        format['ipad'] = ((20,1),(20,2),(29,1),(29,2),(40,1),(40,2),(76,1),(76,2),(83.5,2))
        format['ios-marketing'] = ((1024,1),)
        image = blend_image(self.image,self.color)
    
        data = []
        made = {''}
        for k in format:
            for s in format[k]:
                info = {}
                info['filename'] = self.name
                info['idiom'] = k
                info['size'] = str(s[0])+'x'+str(s[0])
                info['scale'] = str(s[1])+'x'
                if s[1] == 1:
                    suffix = '-'+ info['size'] +'.png'
                else:
                    suffix = '-'+ info['size'] + '@'+str(s[1])+'x.png'
                info['filename'] =  info['filename'] + suffix
                data.append(info)
                
                if (not info['filename'] in made):
                    made.add(info['filename'])
                    d = int(s[0]*s[1])
                    copy = image.resize((d,d))
                    copy.save(os.path.join(*(path+[info['filename']])),'PNG')
        
        contents = {'images':data}
        contents['info'] = { 'version' : 1, 'author' : 'xcode' }
        with open(os.path.join(*(path+['Contents.json'])),'w') as file:
            file.write(json.dumps(contents,indent=2))

    def gen_android(self):
        """
        Generates the icons for Android
        
        The icons will be placed in a subdirectory called `Android`. Inside will be
        several individual folders.  The files in each folder should be copied to
        the equivalent folder in the Android project.  **However** do not copy the
        entire folders as the Android project will have additional files that you
        do not want to delete.
        """    
        print('Generating Android icons')
        root = ['output','android']
        if (not os.path.isdir(os.path.join(*root))):
            os.mkdir(os.path.join(*root))
        
        subdirs = ['mipmap-mdpi','mipmap-hdpi','mipmap-xdpi','mipmap-xxdpi','mipmap-xxxdpi','values']
        for d in subdirs:
            path = root+[d]
            path = os.path.join(*path)
            if (not os.path.isdir(path)):
                os.mkdir(path)
                
        path = root+['values','ic_launcher_background.xml']
        path = os.path.join(*path)
        xml = '&lt;?xml version="1.0" encoding="utf-8"?&gt;\n&lt;resources&gt;\n    &lt;color name="ic_launcher_background"&gt;%s&lt;/color&gt;\n&lt;/resources&gt;\n'
        with open(path,'w') as file:
            file.write(xml % rgb_to_web(self.color))
        
        image = blend_image(self.image,self.color)
        round = circle_image(image)
        float = float_image(self.image)
        
        sizes =  ((48,108),(72,162),(96,216),(144,324),(192,432))
        for p in range(len(sizes)):
            path = root + [subdirs[p],'ic_launcher']
            path = os.path.join(*path)
            d1 = sizes[p][0]
            d2 = sizes[p][1]
            image.resize((d1,d1)).save(path+'.png','PNG')
            round.resize((d1,d1)).save(path+'_round.png','PNG')
            float.resize((d2,d2)).save(path+'_foreground.png','PNG')
    
    def gen_windows(self):
        """
        Generates the icons for Window
        
        This will generate a single .ICO file in the subdirectory called `Windows`. 
        This file should be copied to the appropriate location in your Visual
        Studio Project.
        """    
        print('Generating Windows icons')
        root = ['output','windows']
        if (not os.path.isdir(os.path.join(*root))):
            os.mkdir(os.path.join(*root))
        
        if self.transparent:
            image = self.image
        elif self.rounded:
            image = blend_image(self.image,self.color)
            image = rdrect_image(image)
        else:
            image = blend_image(self.image,self.color)
        
        path = root + [self.name+'.ico']
        image.save(os.path.join(*path),'ICO')

pass
#mark -
#mark COMMAND LINE

def command():
    """
    Executes this script from the command line.
    
    Use the option --help to get help.
    """
    parser = argparse.ArgumentParser(description='A python script to generate icons.')
    parser.add_argument('foreground', type=str, help='the foreground image')
    parser.add_argument('background', type=str, nargs='?', help='the background color')
    parser.add_argument('-t', '--transparent', action='store_true',
                        help='whether to omit the background on desktops')
    parser.add_argument('-r', '--rounded', action='store_true',
                            help='whether to use a round background on desktops')
    args = parser.parse_args()
    IconMaker(args.foreground,args.background,args.transparent,args.rounded).generate()


if __name__ == '__main__':
    command()</pre></body></html>