import sys import pathlib import argparse import subprocess import platform __version_info__ = (1, 2, 0) __version__ = ".".join(map(str, __version_info__)) __copyright__: str = f"Copyright (C) 2018-2022 Declaration of VAR" # default iconset files extension, gets the value from original image ext = ".png" # programs names iconutilProgram = "iconutil" magickProgram = "magick" sipsProgram = "sips" class IconParameters(): width = 0 scale = 1 def __init__(self, width, scale): self.width = width self.scale = scale def getIconName(self): global ext scaleString = "" if self.scale == 1 else f"@{self.scale}x" return f"icon_{self.width}x{self.width}{scaleString}{ext}" def checkProgramInPath(programName, errorMsg=None): checkResult = subprocess.run( ["which", programName], capture_output=True, text=True ) errorMsgString = "" if errorMsg is None else f". {errorMsg}" if checkResult.returncode != 0: raise SystemExit( f"[ERROR] Couldn't find {programName} in your PATH{errorMsgString}" ) else: print(f"Found {programName}: {checkResult.stdout.strip()}") def generateImageConvertingCommand(forSips, originalPicture, ip, iconsetDir): if not forSips: return [ magickProgram, "convert", originalPicture, "-resize", str(ip.width * ip.scale), iconsetDir / ip.getIconName() ] else: return [ sipsProgram, "-z", str(ip.width * ip.scale), str(ip.width * ip.scale), originalPicture, "--out", iconsetDir / ip.getIconName() ] def main(): global ext argParser = argparse.ArgumentParser( prog="generate-iconset", description=" ".join(( f"%(prog)s\n{__copyright__}\nGenerate", "an iconset for a Mac OS application", f"using {iconutilProgram} tool" )), formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False ) argParser.add_argument( "--version", action="version", version=f"%(prog)s {__version__}" ) argParser.add_argument( "image", metavar="/path/image.png", help="path to the original image" ) argParser.add_argument( "--out", metavar="/path/out/", help=" ".join(( "path to the output folder, where to put resulting", ".icns file (default: same folder as original image)" )) ) argParser.add_argument( "--use-sips", action='store_true', help=f"use {sipsProgram} instead of ImageMagick (default: %(default)s)" ) argParser.add_argument( "--force-png", action='store_true', help=" ".join(( "force non-.png original image to be converted", "to .png (default: %(default)s)" )) ) argParser.add_argument( "--ignore-non-mac", action='store_true', help=" ".join(( "allows to run the script on any OS, not only on Mac OS", "(default: %(default)s)" )) ) # argParser.add_argument( # "--delete-tmp-iconset", # action='store_true', # help=" ".join(( # "delete temporary iconset directory,", # "if it already exists (default: %(default)s)" # )) # ) cliArgs = argParser.parse_args() # print(cliArgs) if platform.system() != "Darwin": if not cliArgs.ignore_non_mac: raise SystemExit( " ".join(( "[ERROR] The script is meant to be executed", f"on Mac OS only, as {iconutilProgram} tool is only available", "there. You can ignore this condition", "with --ignore-non-mac" )) ) else: print( " ".join(( "[WARNING] You are running the script not on Mac OS,", "so it is likely to fail,", f"unless you have {iconutilProgram} tool installed" )) ) checkProgramInPath(iconutilProgram) if not cliArgs.use_sips: print("Will use ImageMagick for converting the original image") checkProgramInPath( magickProgram, " ".join(( "Perhaps, you don't have it installed?", f"You can also use {sipsProgram} tool instead", "with --use-sips" )) ) else: print(f"Will use {sipsProgram} for converting the original image") print( " ".join(( "[WARNING] ImageMagick provides better quality results,", f"so do consider using it instead of {sipsProgram}. More details:", "https://decovar.dev/blog/2019/12/12/imagemagick-vs-sips-resize/" )) ) checkProgramInPath(sipsProgram) print() originalPicture = pathlib.Path(cliArgs.image) if not (originalPicture.is_file()): raise SystemExit( f"[ERROR] There is no such image file: {cliArgs.image}" ) print(f"Original image: {originalPicture}") fname = pathlib.Path(originalPicture).stem ext = pathlib.Path(originalPicture).suffix if ext != ".png": if not cliArgs.force_png: print( " ".join(( "[WARNING] Original image extension is not .png,", f"{iconutilProgram} is likely to fail,", "pass --force-png to avoid that" )) ) else: ext = ".png" # destination path for output destDir = ( pathlib.Path(originalPicture).parent if cliArgs.out is None else pathlib.Path(cliArgs.out) ) if not (destDir.is_dir()): try: destDir.mkdir(parents=True) except Exception as ex: raise SystemExit( " ".join(( "[ERROR] The specified output folder doesn't exist", f"and could not be created: {cliArgs.out}" )) ) # path to resulting .icns file resultingIconset = destDir / f"{fname}.icns" # path to temporary iconset folder iconsetDir = pathlib.Path(destDir / f"{fname}.iconset") if not (iconsetDir.is_dir()): try: iconsetDir.mkdir() except Exception as ex: raise SystemExit( " ".join(( "[ERROR] Could not create temporary", f"iconset folder: {iconsetDir}" )) ) else: if False: # cliArgs.delete_tmp_iconset: # not the best idea to let script delete files on disk print("[DEBUG] Deleting temporary iconset folder") else: raise SystemExit( " ".join(( f"[ERROR] Temporary iconset directory ({iconsetDir})", "already exists, you need to", "delete it first" # "either delete it manually", # "or use --delete-tmp-iconset" )) ) # https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/app-icon#app-icon-sizes ListOfIconParameters = [ IconParameters(16, 1), IconParameters(16, 2), IconParameters(32, 1), IconParameters(32, 2), IconParameters(128, 1), IconParameters(128, 2), IconParameters(256, 1), IconParameters(256, 2), IconParameters(512, 1), IconParameters(512, 2) ] print("Converting images for iconset") # generate iconset currentImage = 0 for ip in ListOfIconParameters: currentImage += 1 convertingResult = subprocess.run( generateImageConvertingCommand( cliArgs.use_sips, originalPicture, ip, iconsetDir ), capture_output=True, text=True ) if convertingResult.returncode != 0: raise SystemExit( f"[ERROR] Conversion failed. {convertingResult.stderr.strip()}" ) else: print(f"{currentImage}/{len(ListOfIconParameters)}...") # print(f"Generated {ip.getIconName()}") print("\nGenerating .icns file...") # convert iconset folder to .icns file iconutilResult = subprocess.run( [ iconutilProgram, "-c", "icns", iconsetDir, "-o", resultingIconset ], capture_output=True, text=True ) if iconutilResult.returncode != 0: raise SystemExit( " ".join(( f"[ERROR] {iconutilProgram} could not generate", f"an iconset. {iconutilResult.stderr.strip()}" )) ) else: print( " ".join(( "[SUCCESS] An iconset was successfully", f"generated to {resultingIconset}" )) ) raise SystemExit(0) if __name__ == '__main__': main()