Eric Roller's Development Blog

Bash Script to Scale AppIcons

- Posted in iOS by

The Old Automator Flow

In a previous post, I described an Automator workflow to rescale iOS app icons. While that has worked well, it had potential for improvement:

  1. The workflow document is not easily edited, at least not on a slow machine like mine;

  2. When saved, the workflow document contains a preview image which blows up the file to about 1 GB. The preview image can be removed, but it adds another manual hack that is required.

Because of these shortfalls, I became increasingly reluctant to use it and found myself resizing images manually again.

Welcome to sips

I don't know how long it has been there, but it turns out macOS contains a command-line utility to convert images: sips (scriptable image processing system). You can check if it is installed on your machine using:

$ which sips


$ sips -h
sips - scriptable image processing system.
This tool is used to query or modify raster image files and ColorSync ICC profiles.
Its functionality can also be used through the "Image Events" AppleScript suite.

    sips [image-functions] imagefile ... 
    sips [profile-functions] profile ... 


Using sips

With some tips from this post, I have now set up the following script, saved in a file like ~/bin/resizeAppIcons, (i.e. in a directory listed in your $path variable):


args=$(getopt h: *)     # do not use "$@" to work with 'set' below.
if [ $? != 0 ]; then ((help++)); fi

set -- $args
for i ; do
    case "$i" in
        shift; break;;

if [ $help -gt 0 ]; then
    echo "USAGE: $(basename $0) [-h] [pngFiles...]"
    exit 2

if [ ! -x $pngquant ]; then
    echo "Warning: ImageAlpha.app is not installed"

for img in $* ; do
    echo "$img"

    # Get the current image size: ( width height )
    size=( $(sips -g pixelWidth -g pixelHeight "$img" | grep -o '[0-9]*$') )

    # We expect 1024 x 1024.
    if [[ ${size[0]} -ne ${size[1]} || ${size[0]} -ne 1024 ]]; then
        echo "Error: Image size should be 1024 x 1024 (not ${size[0]} x ${size[1]})."

    # Split the file path:            path/to/filename.png
    dirn=$(dirname "$img")          # path/to
    base=$(basename "${img%.*}")    #         filename
    extn=${img##*.};                 #                  png

    if [[ $extn != "png" ]]; then
        echo "Error: Expected image format is PNG (not $extn)."

    if (( $errs )); then
        echo "Too many errors; giving up"
        exit 1

    # Using all the known image sizes:
    for width in 16 18 19 32 36 38 40 48 55 58 60 64 80 87 88 100 120 128 152 167 172 180 196 216 256 512 1024 ; do

        # Do not overwrite existing files
        if [[ -f "$outfile" ]]; then continue; fi

        echo "--> $outfile"

        # Copy or resize the image
        if [ $width -eq 1024 ]; then
            cp "$img" "$outfile"
            sips -Z $width "$img" --out "$outfile" $gt; /dev/null

        # Use exec in ImageAlpha to reduce colours and size.
        if [[ -x $pngquant ]]; then
            $pngquant -f $colours -o "$outfile" "$outfile"

exit 0

Feel free to adjust the for width in ... line to use the image sizes that you need.


The script requires a 1024 x 1024 pixel PNG image as input. It creates the scaled images in the same directory:

$ resizeAppIcons ./work/export/AppIcon.png
--> ./work/export/AppIcon-16.png
--> ./work/export/AppIcon-512.png
--> ./work/export/AppIcon-1024.png

Image Alpha

Finally, I can recommend the use of ImageAlpha, which will optimize your images and reduce the colours. Once installed in /Applications, the script will pick it up.

For my simple icons, I can get away with just 256 colours. If your icons are more complex, adjust the $colour variable as needed.

Last night, I found myself in this endless loop:

  1. The Mac OS Catalina installer cancels with the message that "macOS could not be installed on your computer: There is not enough free space on your disk to install", offering me to quit the installation or to restart and try again. The only visible button is "Restart".
  2. Trying to delete files using the Terminal application from the installer is fruitless. It never is enough.
  3. Reboot always returns to the installer since the original start volume is no longer bootable. Back to square one.


The only tools available from the installer are Disk Utility and Terminal. Opening Disk Utility reveals a mere 10 GB of space on my disk, where it should have been more than 30 GB.

Running "First Aid", and opening the disclosure triangle to get more information reveals that there are three Time Machine snapshots on my disk. Fine, let’s delete them, but how?

Disk Utility offers no commands to handle Time Machine snapshots.

Where is tmutil?

Numerous solutions articles on the web suggest to use tmutil to manage Time Machine snapshots and settings.

Back in the Terminal, I type tmutil, but all I get is:

-bash-3.2# tmutil
tmutil : command not found

It turns out, the installer image is not a complete macOS system and tmutil is not available.

I also searched the contents of my drive, but to no avail:

-bash-3.2# find /Volume/Dizzy -name tmutil -print

Not knowing which commands are available, I find myself typing a<TAB> to see commands starting with 'a', b<TAB>, then c<TAB>. For d<TAB>, I see diskutil, and this is where it the solution lies.

Solution: diskutil

All the steps in order:

  1. If the menu bar in your installer isn't visible, click on one of the icons in the top-right corner of the screen. The menu bar should become visible.
  2. From the menu bar, open the Terminal application.
  3. You can type diskutil to see the commands that are available:

    -bash-3.2# diskutil
    Disk Utility Tool
    Utility to manage local disks and volumes
    Most commands require an administrator or root user
    WARNING: Most destructive operations are not prompted
    Usage:  diskutil [quiet] <verb> <options>, where <verb> is as follows:
         list                 (List the partitions of a disk)
         apfs <verb>          (Perform additional verbs related to APFS)
    diskutil <verb> with no options will provide help on that verb
  4. Note that there are additional "apfs" commands (verbs):

    -bash-3.2# diskutil apfs
    Usage:  diskutil [quiet] ap[fs] <verb> <options>
            where <verb> is as follows:
         list                (Show status of all current APFS Containers)
         listUsers           (List cryptographic users/keys of an APFS Volume)
         listSnapshots       (List APFS Snapshots in a mounted APFS Volume)
         deleteSnapshot      (Remove an APFS Snapshot from an APFS Volume)
    diskutil apfs <verb> with no options will provide help on that verb
  5. If you don't know the name of your hard disk, you can use diskutil apfs list to see all disks and their names.

  6. Use diskutil apfs listSnapshots to list the snapshots, e.g. for your disk named 'Dizzy':

    -bash-3.2# diskutil apfs listSnapshots Dizzy
    Snapshots for disk1s1 (3 found)
    +-- F5D46466-3269-4480-BA1A-8BE23DF1800
    |   Name:        com.apple.TimeMachine.2019-10-07-205243
    |   XID:         2201791
    |   Purgeable:   Yes
  7. To delete a snapshop, use diskutil apfs deleteSnapshot with the UUID copied from the list above:

    -bash-3.2# diskutil apfs deleteSnapshot Dizzy -uuid F5D46466-3269-4480-BA1A-8BE23DF1800
    Deleting APFS Snapshot F5D46466-3269-4480-BA1A-8BE23DF1800 "com.apple.TimeMachine.2019-10-07-205243" from APFS Volume disk1s1
    Started APFS operation
    Finished APFS operation
  8. Repeat for the other snapshots.

In my case, that freed up 50 GB on my disk, allowing the macOS Catalina installer to continue.

[Update 2019-10-21: Corrected typo 'apgf' -> 'apfs']

Resizing Finder Columns

- Posted in macOS by

I enjoy using the column view in the macOS Finder, but more more often than not, I need to resize the columns to be able to read all the file or folder names.

Certainly, resizing is easily done by dragging the vertical column separators to the right until all names are readable. What has been bugging me, however, is that when I do that for the right-most column, I often end up inadvertently resizing the window at the same time.

How to resize the right-most column without resizing the window?

It turns out, you can Ctrl-click (or right-click) on a column separator to open a context menu:

Ctrl-click on a column separator

There you can chose to:

  • Right Size This Column to resize the column to the left of the separator;

  • Right Size All Columns Individually to resize all columns such that they are just as wide as they need to be to be able to read all file and folder names;

  • Right Size All Columns Equally to resize all columns such that they are all equally wide and readable.

So, if you chose one of the Resize All Columns… options, the right-most column will be resized. But, if you had selected the right-most separator, the window may still resize!

Therefore: Ctrl-click on one of the other column separators, not the right-most one. When resizing is initiated there, the window size will not be changed.

Finally, here is the best part:

This also works in a standard macOS "File Open" or "Save As" dialog!


[2019-08-16] You can also double-click on the column separator with your mouse:

Double-click on the column separator to Right Size This Column.

Option+Double-click on the column separator to Right Size All Columns Individually.

Here is a piece of code that looks quite reasonable, but it doesn't work. The plan is to assemble a playlist of multiple songs starting off with a list of their persistent IDs:

let persistentSongIDs : [MPMediaEntityPersistentID] = ...

let musicPlayer = MPMusicPlayerController.applicationQueuePlayer
var filterSet = Set<MPMediaPredicate>()

for songID in persistentSongIDs {
    let predicate = MPMediaPropertyPredicate(value: songID,
            forProperty: MPMediaItemPropertyPersistentID)

if filterSet.isEmpty == false {
    let query = MPMediaQuery(filterPredicates: filterSet)
    let descriptor = MPMusicPlayerMediaItemQueueDescriptor(query: query)
    mediaPlayer.setQueue(with: descriptor)

So, why does it not work? We are creating search predicates with each of the persistent song IDs, surely this should give us a list of songs that we can play, right?

Well, no.

As soon as you have multiple songs, the song list will always be empty.

The reason for this is simple, if you think about it. What we are searching for is:

"persistentID == 5819395988480015566"
"persistentID == 9054558313999882624"
"persistentID == 8511246475365999992"

But all of these search predicates are combined, i.e. they all need to be true to find matches.

Let's rephrase this: This will never be true:

"persistentID == A" && "persistentID == B" && ...

So How Do We Fix This?

We need to search for this:

"persistentID == A" || "persistentID == B" || ...

But that cannot be expressed in a list of search predicates. The solution is to search for each ID in turn and assemble a playlist manually:

mySongs = [MPMediaItem]()

for songID in persistentSongIDs {
    let predicate = MPMediaPropertyPredicate(value: songID,
            forProperty: MPMediaItemPropertyPersistentID)
    let query = MPMediaQuery(filterPredicates: [predicate])
    if let song = query.items?.first {

if mySongs.isEmpty == false {
    // Let's play !!!
    musicPlayer.setQueue(with: MPMediaItemCollection(items: mySongs))