Creating A Proper ICNS/ICO Icon File From An Image

Sometimes it’s nice to override the default icons for various folders and drives on your Mac. Fortunately, the Mac makes it trivial to do this, as you can just drag an image into the icon in the “Get Info” window of a file in the Finder. I used to just do that with empty folders to keep a copy of the icon, but sometimes in moving said folder around, the icon gets lost, which is frustrating when you had that perfect icon.

Apple does provide the tools you need to generate an icns file that contains all the icon data in a form that will not get lost. There is also a command line utility you can use to create scaled images.

I also added the creation of Windows .ico files. This part relies on a tool I took from the MacPorts port icoutils called icotool. Windows icons are uglier to install than the simple copy and paste on the Mac, but it is not too hard. You have to create an autorun.inf file and the .ico file, put them both in the root of the drive, and then set the Hidden attribute on them so they do not uglify your drive. The autorun.inf file looks like:

[Autorun]
Icon="drive.ico"

I put the two together into a Mac only script that will take an image and properly scale it, and generate the ICNS file, the ICO file, and an autorun.inf file and, if you specify a volume as a second parameter, will install the icon for you:

#!/bin/bash

trap 'rm -rf /tmp/*.$$ *.$$ *.$$.* icon.$$.iconset; exit 0' 0 1 2 3 13 15

# This script will take a PNG image. It should be square, and at least
# 512x512; preferably 1024x1024, and works best if it has a transparency
# layer.  It will create images of all the appropriate sizes, then
# create an icns file that can be used for whatever icons you want.

# Grab the name of the file.  It accepts 1 or 2 arguments.  Everything else
# will be ignored.
#
# The first argument is required, and is the name of the image.
# The second argument is the (optional) name of a volume to install
# the icon so it will appear as the custom icon.

input_file="$1"
destination_volume="$2"

if [ ! "${input_file}" ]
then
	echo "Please provide a png image name, and optionally, the"
	echo "full path to a volume you want to install the icon on."
	exit 1
fi

# Figure out the extension of the file, and remove it from the variable. Case
# doesn't matter.
ext="$( echo "${input_file}" | awk -F'.' '{ print $NF; }' | tr '[A-Z]' '[a-z]' )"

if [ "${ext}" != "png" ]
then
	echo "This tool requires a PNG file (extension .png)."
	exit 1
fi

output="$( basename "${input_file}" ".${ext}" )"

pixelHeight=$( sips -g pixelHeight "${input_file}" | grep "pixelHeight:" | tail -n 1 | awk '{ print $2; }' )
pixelWidth=$( sips -g pixelWidth "${input_file}" | grep "pixelWidth:" | tail -n 1 | awk '{ print $2; }' )

if [ ${pixelHeight} -lt 1024 ] && [ ${pixelWidth} -lt 1024 ]
then
	echo "Warning: Image should have at least one dimension >= 1024."
fi

hasAlpha=$( sips -g hasAlpha "${input_file}" | grep "hasAlpha:" | awk '{ print $2; }' )

if [ "${hasAlpha}" != "yes" ]
then
	echo "Warning: Icons work best if the image has a transparency layer."
fi

# Scale the input to 1024x1024, padding as necessary to make it
# square without changing the aspect ratio.
sips -Z 1024 -p 1024 1024 "${input_file}" -o "${output}.$$.png" > /dev/null
input_file="${output}.$$.png"

# Create a temp folder for all the required icon sizes, and then
# generate images at the appropriate sizes as defined by Apple:
# https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/app-icon/

mkdir -p "icon.$$.iconset"

sips -z 16 16     "${input_file}" --out "icon.$$.iconset/icon_16x16.png" > /dev/null
sips -z 32 32     "${input_file}" --out "icon.$$.iconset/icon_16x16@2x.png" > /dev/null
sips -z 32 32     "${input_file}" --out "icon.$$.iconset/icon_32x32.png" > /dev/null
sips -z 64 64     "${input_file}" --out "icon.$$.iconset/icon_32x32@2x.png" > /dev/null
sips -z 128 128   "${input_file}" --out "icon.$$.iconset/icon_128x128.png" > /dev/null
sips -z 256 256   "${input_file}" --out "icon.$$.iconset/icon_128x128@2x.png" > /dev/null
sips -z 256 256   "${input_file}" --out "icon.$$.iconset/icon_256x256.png" > /dev/null
sips -z 512 512   "${input_file}" --out "icon.$$.iconset/icon_256x256@2x.png" > /dev/null
sips -z 512 512   "${input_file}" --out "icon.$$.iconset/icon_512x512.png" > /dev/null
sips -z 1024 1024 "${input_file}" --out "icon.$$.iconset/icon_512x512@2x.png" > /dev/null

# Merge all of them into an icns file, with the same name as the input file.
iconutil -c icns "icon.$$.iconset" -o "${output}.icns"

# Create a Windows ICO file, if the tool is installed.

if [ "$( which icotool )" ]
then
{
	icotool --icon -c -o "${output}.ico" icon.$$.iconset/*.png
	echo -ne "[Autorun]\r\nIcon=\"${output}.ico\"\r\n" > autorun.inf

	if [ ! "${destination_volume}" ]
	then
		echo "${output}.ico and autorun.inf are for Windows."
		echo "Once you've copied them over to an ExFAT volume,"
		echo "run the command: chflags hidden autorun.inf \"${output}.ico\""
		echo "Or, when you connect it to a Windows machine, you can set the hidden"
		echo "properties from the explorer."
	fi
}
else
	echo "To generate Windows ico files, install the MacPorts package \"icoutils\"."
fi

# If a volume is specified, copy/install the icon(s) to the Volume

if [ -d "${destination_volume}" ]
then
{
	if [ -f "${output}.ico" ]
	then
		cp "${output}.ico" autorun.inf "${destination_volume}"
		chflags hidden "${destination_volume}"/"${output}.ico" "${destination_volume}"/autorun.inf
	fi

	cp "${output}.icns" "${destination_volume}"/.VolumeIcon.icns
	chflags hidden "${destination_volume}"/.VolumeIcon.icns

	# This sets an attribute on the volume to tell it there is a custom icon.  It must be
	# exactly 32 bytes long or it will give an error.

	# https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-8A428/Finder.h

	# First, make sure there is already a custom attribute "com.apple.FinderInfo"
	# block set.  If so, then set the appropriate bit in the block.
	if [ "$( xattr -l "${destination_volume}" | grep "com.apple.FinderInfo" )" ]
	then
		# Grab the current attributes of the volume, and put it in an array
		ATTRIBUTES=( $( xattr -px com.apple.FinderInfo "${destination_volume}" ) )

		# OR the 9th field with 0x4 to set the "custom icon present" bit on the attributes
		ATTRIBUTES[8]=$( printf "%02x" $(( ${ATTRIBUTES[8]} | 16#4 )) )
	else
		# Since there is no attribute com.apple.FinderInfo, we hard set
		# just the one bit that needs to be set to tell the Finder there
		# is a custom icon present.
		ATTRIBUTES=( 00 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 )
	fi
	# Here we set the new value.
	xattr -wx com.apple.FinderInfo "${ATTRIBUTES[*]}" "${destination_volume}"
}
fi

Leave a Comment