Programmatically Adding an Icon to a Folder or File

I recently had the need to programmatically add an icon to a folder in OS X, with the source being a PNG image file. This arose in the context of setting up an app installer, which needed to set custom Finder icons on several folders. Another context in which a programmatic solution would be useful would be in a running desktop application, where new folders needing custom icons might be created (or their icons be modified for some reason).

Doing this programmatically should be trivially easy, but there are problems with solutions that are commonly provided in answer to this need. This post discusses some of those solutions and their shortcomings, and presents a usable workaround or two. It’s also a plea (or two) for help…

If you do a web search for programmatically changing or setting a custom Finder icon, there are quite a few hits; all  of them present minor variations on a few types of solutions. Unfortunately, they either only “almost work”, or they run into what is either a serious bug or an undocumented feature.

 The Most Frequently Proposed Technique: You’re Doing It Wrong

A number of variants of the commonly proposed solutions are presented in this page on stackexchange.  This one uses a sequence of command-line functions (in a shell script) to convert a PNG image to a resource, which is then attached to the file/folder, and used as the icon:

replace_icon(){
    droplet=$1
    icon=$2
    if [[ $icon =~ ^https?:// ]]; then
        curl -sLo /tmp/icon $icon
        icon=/tmp/icon
    fi
    rm -rf $droplet$'/Icon\r'
    sips -i $icon >/dev/null
    DeRez -only icns $icon > /tmp/icns.rsrc
    Rez -append /tmp/icns.rsrc -o $droplet$'/Icon\r'
    SetFile -a C $droplet
    SetFile -a V $droplet$'/Icon\r'
}

This sequence of commands almost works. The problem, though, is that the resulting folder/file icon does not have full resolution; this can be seen by switching the Finder to icon view, and scaling the icon size. In this image, the folder on the left has had its icon set with the method just shown, and the folder on the right has had its icon set manually:ResProblem

The problem here is with the sips -i command: this creates a Finder icon for the image file. Unfortunately, the resulting icon is relatively low-resolution. Indeed, the PNG file used as an argument to this shell script ends up with the same low-res Finder icon as does the target folder. So the solution here is to simply manually add the Finder icon to the PNG file, and forgo the sips -i command in the script (by commenting it out). First plea for help: is there a more sophisticated use of sips (or an alternative command) that yields a full-resolution icon?

One potential issue with this solution is with the various commands themselves. In older posts that contain variants of this solution, there are dire warnings regarding one’s machine needing Apple’s Developer Tools installed, lest any of the commands be unavailable.  On my machines, these tools are all in /usr/bin, but since I’ve installed Xcode, etc. on all of them, I cannot really be sure whether these commands might actually be available on anyone’s machine, or not. This could be a problem in the case where you have an app installer that’s being run on some random customer’s machine.

But the takeaway here (so far) is that this often-cited approach will work just fine, provided you avoid the sips -i command, and ensure in some fashion that your icon file has a proper Finder icon, and can be certain that all users’ machines have the commands installed.

Oh, one other thing — this really only works on Folders, and not files… Here’s a proposed solution (from that same forum thread I linked to earlier) for a solution for files:

# Take an image and make the image its own icon:
sips -i icon.png

# Extract the icon to its own resource file:
DeRez -only icns icon.png > tmpicns.rsrc

# append this resource to the file you want to icon-ize.
Rez -append tmpicns.rsrc -o file.ext

# Use the resource to set the icon.
SetFile -a C file.ext

# clean up.
rm tmpicns.rsrc
# rm icon.png # probably want to keep this for re-use.

This one has that same issue with the sips -i command. But, otherwise it should work in plain files; it does not work on folders, though. Not a terribly nice general solution, then, but OK if you want to discriminate between files and folders in your script, and then invoke the right code in each case (the commands from the earlier block of code, for folders, and from this second block, for files). Just avoid that sips -i

The Programmer’s Approach: Not Quite Right

A natural response from a programmer might be to simply write some code to do this.  Such a program could be invoked similarly to running one of the above shell scripts, for use on the command line or in an installer; as well, a function that can place a Finder icon on a file or folder could be generally useful. Well, of course this can be, and has been, done; for example, in this Cocoa is My Girlfriend post. Quite a lot of code, which fortunately has been almost entirely obviated by the addition of this API:

[[NSWorkspace sharedWorkspace] setIcon:image forFile:filePath options:0];

A nice little Xcode project utilizing this API can be found here.

Almost criminally easy. But there’s a major problem with this, which I’ll describe shortly. As a relevant aside, on that same now-often-referenced forum there is a post that shows you can essentially do this with a few lines of Python:

#!/usr/bin/python
from AppKit import NSWorkspace
import sys

for path in sys.argv[1:]:
    NSWorkspace.sharedWorkspace().setIcon_forFile_options_(None, path, 0)

or this:

#!/usr/bin/env python
import Cocoa
import sys

Cocoa.NSWorkspace.sharedWorkspace().setIcon_forFile_options_(Cocoa.NSImage.alloc().initWithContentsOfFile_(sys.argv[1].decode('utf-8')), sys.argv[2].decode('utf-8'), 0) or sys.exit("Unable to set file icon")

Cool. But let’s get to that major problem, which is exhibited by both that direct programmatic use in Objective-C, and (unsurprisingly) in the Python script version: the Finder icon comes out the wrong color. The image below shows the same pair of images from the last example; but instead of the excessively low resolution issue, we have an incorrect color issue:

ColorProblem

Applying the same process to the default folder icon from Yosemite, we get this:

So, just to be clear: if we use the manual copy-paste scheme, we get a rather different result than if we copy with Cocoa commands in a program.

It’s been suggested this is a manifestation of this reported bug, wherein the posters suggest this may be related to alpha premultiplication. On the other hand, the color shifts you can see in my icon aren’t consistent with an alpha-related problem. It’s also been suggested that the problem may be due to color space/profile mismatches — that is, when using the Cocoa programming approach, we’d need to be sure the target and destination were assuming the same color space/profile. But, given the wide variety of these,  experimentation would be quite tedious.

A Limited Workaround

For the case of a custom Finder icon for a folder, there’s a simple approach that avoids the color-shift problem, as well as the Rez/DeRez steps. The Finder icon for a folder is associated with a hidden file inside the folder, so if we have an existing folder with our desired icon on it, we can simply copy that hidden file to our target folder:

#!/bin/sh

# Usage
if [ "$#" -ne 2 ] || ! [ -f "$1" ] || ! [ -d "$2" ]; then
    echo "Usage: $0 SOURCE DEST" >&2
    exit 1
fi

# Local variables
SOURCE=$1
DEST=$2

# Remove existing
rm -f $DEST$'/Icon\r'

# Copy hidden file
cp $SOURCE$'/Icon\r' $DEST/.

# Use file as Finder icon
SetFile -a C $DEST

# Make sure hidden file is actually hidden from the Finder
SetFile -a V $DEST$'/Icon\r'

It should be noted that this could, in theory, fail to work in future versions of OS X. As well, this obviously only works for Folders, not files.

Ideally, the Cocoa-based approach would be best: it’s easy to write a small app to do this for command-line usage, or as part of an installer, and essentially the same code can be used within an application to set or change custom Finder icons on files and folders. If the color-shift problem is due to color spaces/profiles, then perhaps some reader out there can suggest a solution; on the other hand, the bug report I referenced suggests that there may be some alpha-related issue as well (or instead?). Please give a shout if you have any insights!