""" Icon and LaunchImage generator for iOS ====================================== .. author:: Mathieu Virbel """ # flake8: noqa (E121 mainly) __all__ = ["launchimage"] import sh import json from PIL import Image from os.path import join, exists from os import makedirs appicon_json = { "images": [ { "idiom": "iphone", "size": "20x20", "scale": "2x", "filename": "Icon40.png" }, { "idiom": "iphone", "size": "20x20", "scale": "3x", "filename": "Icon60.png" }, { "size": "29x29", "idiom": "iphone", "filename": "Icon29.png", "scale": "1x" }, { "size": "29x29", "idiom": "iphone", "filename": "Icon58.png", "scale": "2x" }, { "size": "29x29", "idiom": "iphone", "filename": "Icon87.png", "scale": "3x" }, { "size": "40x40", "idiom": "iphone", "filename": "Icon80.png", "scale": "2x" }, { "size": "40x40", "idiom": "iphone", "filename": "Icon120.png", "scale": "3x" }, { "size": "57x57", "idiom": "iphone", "filename": "Icon57.png", "scale": "1x" }, { "size": "57x57", "idiom": "iphone", "filename": "Icon114.png", "scale": "2x" }, { "size": "60x60", "idiom": "iphone", "filename": "Icon120.png", "scale": "2x" }, { "size": "60x60", "idiom": "iphone", "filename": "Icon180.png", "scale": "3x" }, { "idiom": "ipad", "size": "20x20", "filename": "Icon20.png", "scale": "1x" }, { "idiom": "ipad", "size": "20x20", "filename": "Icon40.png", "scale": "2x" }, { "size": "29x29", "idiom": "ipad", "filename": "Icon29.png", "scale": "1x" }, { "size": "29x29", "idiom": "ipad", "filename": "Icon58.png", "scale": "2x" }, { "size": "40x40", "idiom": "ipad", "filename": "Icon40.png", "scale": "1x" }, { "size": "40x40", "idiom": "ipad", "filename": "Icon80.png", "scale": "2x" }, { "size": "50x50", "idiom": "ipad", "filename": "Icon50.png", "scale": "1x" }, { "size": "50x50", "idiom": "ipad", "filename": "Icon100.png", "scale": "2x" }, { "size": "72x72", "idiom": "ipad", "filename": "Icon72.png", "scale": "1x" }, { "size": "72x72", "idiom": "ipad", "filename": "Icon144.png", "scale": "2x" }, { "size": "76x76", "idiom": "ipad", "filename": "Icon76.png", "scale": "1x" }, { "size": "76x76", "idiom": "ipad", "filename": "Icon152.png", "scale": "2x" }, # If activated, we got a submission error: # "Error ITMS-9000: Invalid Image Path - No image found at the path # referenced under key 'CFBundleIcons': 'AppIcon120x120'" # { # "size": "120x120", # "idiom": "car", # "filename": "Icon120.png", # "scale": "1x" # }, { "size": "24x24", "idiom": "watch", "scale": "2x", "filename": "Icon48.png", "role": "notificationCenter", "subtype": "38mm" }, { "size": "27.5x27.5", "idiom": "watch", "scale": "2x", "filename": "Icon55.png", "role": "notificationCenter", "subtype": "42mm" }, { "size": "29x29", "idiom": "watch", "filename": "Icon58.png", "role": "companionSettings", "scale": "2x" }, { "size": "29x29", "idiom": "watch", "filename": "Icon87.png", "role": "companionSettings", "scale": "3x" }, { "size": "40x40", "idiom": "watch", "scale": "2x", "filename": "Icon80.png", "role": "appLauncher", "subtype": "38mm" }, { "size": "44x44", "idiom": "watch", "scale": "2x", "filename": "Icon88.png", "role": "longLook", "subtype": "42mm" }, { "size": "86x86", "idiom": "watch", "scale": "2x", "filename": "Icon172.png", "role": "quickLook", "subtype": "38mm" }, { "size": "44x44", "idiom": "watch", "scale": "2x", "filename": "Icon88.png", "role": "appLauncher", "subtype": "40mm" }, { "size": "50x50", "idiom": "watch", "scale": "2x", "filename": "Icon100.png", "role": "appLauncher", "subtype": "44mm" }, { "size": "98x98", "idiom": "watch", "scale": "2x", "filename": "Icon196.png", "role": "quickLook", "subtype": "42mm" }, { "size": "108x108", "idiom": "watch", "scale": "2x", "filename": "Icon216.png", "role": "quickLook", "subtype": "44mm" }, { "size": "16x16", "idiom": "mac", "filename": "Icon16.png", "scale": "1x" }, { "size": "16x16", "idiom": "mac", "filename": "Icon32.png", "scale": "2x" }, { "size": "32x32", "idiom": "mac", "filename": "Icon32.png", "scale": "1x" }, { "size": "32x32", "idiom": "mac", "filename": "Icon64.png", "scale": "2x" }, { "size": "128x128", "idiom": "mac", "filename": "Icon128.png", "scale": "1x" }, { "size": "128x128", "idiom": "mac", "filename": "Icon256.png", "scale": "2x" }, { "size": "256x256", "idiom": "mac", "filename": "Icon256.png", "scale": "1x" }, { "size": "256x256", "idiom": "mac", "filename": "Icon512.png", "scale": "2x" }, { "size": "512x512", "idiom": "mac", "filename": "Icon512.png", "scale": "1x" }, { "size": "512x512", "idiom": "mac", "filename": "Icon1024.png", "scale": "2x" }, { "size": "83.5x83.5", "idiom": "ipad", "filename": "Icon167.png", "scale": "2x" }, { "idiom": "ios-marketing", "size": "1024x1024", "scale": "1x", "filename": "Icon1024.png" }, { "idiom": "watch-marketing", "size": "1024x1024", "scale": "1x", "filename": "Icon1024.png" }, ], "info": { "version": 1, "author": "xcode" }, # "properties": { # "pre-rendered": True # } } launchimage_json = { "images": [ { "extent": "full-screen", "idiom": "iphone", "subtype": "736h", "filename": "Default1242x2208.png", "minimum-system-version": "8.0", "orientation": "portrait", "scale": "3x" }, { "extent": "full-screen", "idiom": "iphone", "subtype": "736h", "filename": "Default2208x1242.png", "minimum-system-version": "8.0", "orientation": "landscape", "scale": "3x" }, { "extent": "full-screen", "idiom": "iphone", "subtype": "667h", "filename": "Default750x1334.png", "minimum-system-version": "8.0", "orientation": "portrait", "scale": "2x" }, { "orientation": "portrait", "idiom": "iphone", "extent": "full-screen", "minimum-system-version": "7.0", "filename": "Default640x960.png", "scale": "2x" }, { "extent": "full-screen", "idiom": "iphone", "subtype": "retina4", "filename": "Default640x1136.png", "minimum-system-version": "7.0", "orientation": "portrait", "scale": "2x" }, { "orientation": "portrait", "idiom": "ipad", "extent": "full-screen", "minimum-system-version": "7.0", "filename": "Default768x1024.png", "scale": "1x" }, { "orientation": "landscape", "idiom": "ipad", "extent": "full-screen", "minimum-system-version": "7.0", "filename": "Default1024x768.png", "scale": "1x" }, { "orientation": "portrait", "idiom": "ipad", "extent": "full-screen", "minimum-system-version": "7.0", "filename": "Default1536x2048.png", "scale": "2x" }, { "orientation": "landscape", "idiom": "ipad", "extent": "full-screen", "minimum-system-version": "7.0", "filename": "Default2048x1536.png", "scale": "2x" }, { "orientation": "portrait", "idiom": "iphone", "extent": "full-screen", "filename": "Default320x480.png", "scale": "1x" }, { "orientation": "portrait", "idiom": "iphone", "extent": "full-screen", "filename": "Default640x960.png", "scale": "2x" }, { "orientation": "portrait", "idiom": "iphone", "extent": "full-screen", "filename": "Default640x1136.png", "subtype": "retina4", "scale": "2x" }, { "orientation": "portrait", "idiom": "ipad", "extent": "full-screen", "filename": "Default768x1024.png", "scale": "1x" }, { "orientation": "landscape", "idiom": "ipad", "extent": "full-screen", "filename": "Default1024x768.png", "scale": "1x" }, { "orientation": "portrait", "idiom": "ipad", "extent": "full-screen", "filename": "Default1536x2048.png", "scale": "2x" }, { "orientation": "landscape", "idiom": "ipad", "extent": "full-screen", "filename": "Default2048x1536.png", "scale": "2x" }, ], "info": { "version": 1, "author": "xcode" } } def icon(image_xcassets, image_fn): """Generate all the possible Icon from a single image_fn """ appicon_dir = join(image_xcassets, "AppIcon.appiconset") if not exists(appicon_dir): makedirs(appicon_dir) with open(join(appicon_dir, "Contents.json"), "w") as fd: json.dump(appicon_json, fd) options = ( # iPhone # Spotlight - iOS 5,6 # Settings - iOS 5-8 # 29pt - 1x,2x,3x ("87", None, "Icon87.png"), ("58", None, "Icon58.png"), ("29", "Icon58.png", "Icon29.png"), # iPhone notification # 20pt - 2x,3x # ("40", None, "Icon40.png"), ("60", None, "Icon60.png"), # iPhone # Spotlight - iOS 7-8 # 40pt 2x,3x ("120", None, "Icon120.png"), ("80", None, "Icon80.png"), # iPhone # App - iOS 5,6 # 57pt 1x,2x ("114", None, "Icon114.png"), ("57", "Icon114.png", "Icon57.png"), # iPhone # App - iOS 7,8 # 60pt 2x,3x ("180", None, "Icon180.png"), # ("120", None, "Icon120.png # duplicate"), # iPad # Notifications # 20pt 1x,2x ("20", "Icon80.png", "Icon20.png"), ("40", "Icon80.png", "Icon40.png"), # iPad # Settings iOS 5-8 # ("58", None, "Icon58.png # duplicate"), # ("29", "Icon58.png", "Icon29.png # duplicate"), # iPad # Spotlight iOS 7,8 # 40pt 1x,2x # ("80", None, "Icon80.png # duplicate"), ("40", "Icon80.png", "Icon40.png"), # iPad # Spotlight iOS 5,6 # 50pt 1x,2x ("100", None, "Icon100.png"), ("50", "Icon100.png", "Icon50.png"), # iPad # App iOS 5,6 # 72pt 1x,2x ("144", None, "Icon144.png"), ("72", "Icon144.png", "Icon72.png"), # iPad # App iOS 7,8 # 76pt 1x,2x ("152", None, "Icon152.png"), ("76", "Icon152.png", "Icon76.png"), # iPad # App iOS 9 # 83.5pt 2x ("167", None, "Icon167.png"), # CarPlay # App iOS 8 # 120pt 1x # ("120", None, "Icon120.png # duplicate"), # Apple Watch # Notification Center # 38mm, 42mm ("48", None, "Icon48.png"), ("55", None, "Icon55.png"), # Apple Watch # Companion Settings # 29pt 2x,3x # ("58", None, "Icon58.png # duplicate"), # ("87", None, "Icon87.png # duplicate"), # Apple Watch # Home Screen (All) # Long Look (38mm) # ("80", None, "Icon80.png # duplicate"), # Apple Watch # Long Look (42mm) ("88", None, "Icon88.png"), # Apple Watch # Short Look # 38mm, 42mm, 44mm ("172", None, "Icon172.png"), ("196", None, "Icon196.png"), ("216", None, "Icon216.png"), # OS X # 512pt 1x,2x ("1024", None, "Icon1024.png"), ("512", "Icon1024.png", "Icon512.png"), # OS X # 256pt 1x,2x # ("512", "Icon1024.png", "Icon512.png # duplicate"), ("256", "Icon512.png", "Icon256.png"), # OS X # 128pt 1x,2x # ("256", "Icon512.png", "Icon256.png # duplicate"), ("128", "Icon256.png", "Icon128.png"), # OS X # 32pt 1x,2x ("64", "Icon128.png", "Icon64.png"), ("32", "Icon64.png", "Icon32.png"), # OS X # 16pt 1x,2x # ("32", "Icon64.png", "Icon32.png # duplicate"), ("16", "Icon32.png", "Icon16.png")) _generate("AppIcon.appiconset", image_xcassets, image_fn, options, icon=True) def launchimage(image_xcassets, image_fn): """Generate all the possible Launch Images from a single image_fn """ launchimage_dir = join(image_xcassets, "LaunchImage.launchimage") if not exists(launchimage_dir): makedirs(launchimage_dir) with open(join(launchimage_dir, "Contents.json"), "w") as fd: json.dump(launchimage_json, fd) options = ( # size, input, output # iPhone 3.5" @2x ("640 960", None, "Default640x960.png"), # iPhone 3.5" @1x ("320 480", None, "Default320x480.png"), # iPhone 4.0" @2x ("640 1136", None, "Default640x1136.png"), # iPhone 5.5" @3x - landscape ("2208 1242", None, "Default2208x1242.png"), # iPhone 5.5" @3x - portrait ("1242 2208", None, "Default1242x2208.png"), # iPhone 4.7" @2x ("750 1334", None, "Default750x1334.png"), # iPad @2x - landscape ("2048 1536", None, "Default2048x1536.png"), # iPad @2x - portrait ("1536 2048", None, "Default1536x2048.png"), # iPad @1x - landscape ("1024 768", None, "Default1024x768.png"), # iPad @1x - portrait ("768 1024", None, "Default768x1024.png"), ) _generate("LaunchImage.launchimage", image_xcassets, image_fn, options) def _buildimage(in_fn, out_fn, size, padcolor=None): im = Image.open(in_fn) # read the first left/bottom pixel bgcolor = im.getpixel((0, 0)) # ensure the image fit in the destination size if im.size[0] > size[0] or im.size[1] > size[1]: f = max(im.size[0] / size[0], im.size[1] / size[1]) newsize = int(im.size[0] / f), int(im.size[1] / f) im = im.resize(newsize) # create final image outim = Image.new("RGB", size, bgcolor[:3]) x = (size[0] - im.size[0]) // 2 y = (size[1] - im.size[1]) // 2 outim.paste(im, (x, y)) # save the image outim.save(out_fn) def _generate(d, image_xcassets, image_fn, options, icon=False): for c, in_fn, out_fn in options: args = [] if in_fn is not None: filename = join(image_xcassets, d, in_fn) else: filename = image_fn if icon: args += ["-Z", c] args += [ "--out", join(image_xcassets, d, out_fn) ] print("sips", " ".join(args)) sh.sips(*args) else: size = [int(x) for x in c.split()] _buildimage(filename, join(image_xcassets, d, out_fn), size)