Skip to content

Instantly share code, notes, and snippets.

@joevt
Last active July 17, 2024 06:09
Show Gist options
  • Save joevt/9fa524ebbef3db46842f14f33cf64ca5 to your computer and use it in GitHub Desktop.
Save joevt/9fa524ebbef3db46842f14f33cf64ca5 to your computer and use it in GitHub Desktop.
Script for working with macOS icns files
#!/bin/bash
# joevt Jul 16, 2024
alias icns2icns=/Volumes/Work/Programming/XcodeProjects/Icons/osxiconutils/joevt-osxiconutils/DerivedData/osxiconutils/Build/Products/Debug/icns2icns
alias icns2image=/Volumes/Work/Programming/XcodeProjects/Icons/osxiconutils/joevt-osxiconutils/DerivedData/osxiconutils/Build/Products/Debug/icns2image
alias image2icns=/Volumes/Work/Programming/XcodeProjects/Icons/osxiconutils/joevt-osxiconutils/DerivedData/osxiconutils/Build/Products/Debug/image2icns
patmatch () {
# substitute for [[ =~ ]] for Mac OS X 10.4
perl -0777 -ne '<>; exit !( $_ =~ /'"$1"'/ )'
}
dumpicnslist () {
local theicon="$1"
local dumps="$2"
local filelen="$3"
local pos="$4"
while (( pos < filelen )); do
local datapos=0
local datalen=0
local header=""
local blob=""
blob="$(xxd -s "$pos" -l 32 -p -c 32 "$theicon")"
local datatypehex="${blob:0:8}"
local datatype=""
datatype="$(printf "\x${blob:0:2}\x${blob:2:2}\x${blob:4:2}\x${blob:6:2}")"
printf "%08X" "$pos" # because Mac OS X 10.4.11 xxd doesn't support -o option
if ((pos == 0)) && [[ $datatypehex = $'\x89PNG' ]]; then
((datapos = pos))
((datalen = filelen - pos))
datatype="PNG"
header="${blob:0:24}"
printf "%s" "${blob}" | xxd -r -p | xxd -g 4 -l 32 -c 32 | sed -E 's/^[^:]*//'
else
datalen="$((0x${blob:8:8}))"
if ((datalen < 8)); then
datalen=8
fi
blob="${blob:0:$datalen*2}"
local header="${blob:16:24}"
((datapos=pos+8))
((datalen-=8))
if [[ $datatype = "TOC " ]]; then
printf "%s" "${blob:0:16}" | xxd -r -p | xxd -g 4 -l 32 -c 32 | sed -E 's/^[^:]*//'
xxd -g 4 -s $datapos -l $datalen -c 32 "$theicon" | sed "s/^/ /"
echo
# lang=C elif [[ $datatype =~ $'icns|slct|sbtp|drop|odrp|open|over|tile|\xFD\xD9\x2F\xA8' ]]; then # not compatible with 10.4.11
elif lang=C patmatch 'icns|slct|sbtp|drop|odrp|open|over|tile|\xFD\xD9\x2F\xA8' <<< "$datatype" ; then # compatible with 10.4.11
printf "%s" "${blob:0:16}" | xxd -r -p | xxd -g 4 -l 32 -c 32 | sed -E 's/^[^:]*//'
dumpicnslist "$theicon" "${dumps}" $((datapos+datalen)) $datapos
else
printf "%s" "${blob}" | xxd -r -p | xxd -g 4 -l 32 -c 32 | sed -E 's/^[^:]*//'
fi
fi
if [[ -n $dumps ]]; then
case $header in
89504e470d0a1a0a*)
dd "if=$theicon" bs=1 skip=$datapos count=$datalen "of=${dumps}_$(printf "%06x" "$pos")_${datatype}.png" 2> /dev/null
;;
0000000c6a5020200d0a870a*)
dd "if=$theicon" bs=1 skip=$datapos count=$datalen "of=${dumps}_$(printf "%06x" "$pos")_${datatype}.jp2" 2> /dev/null
;;
ff4fff51*)
dd "if=$theicon" bs=1 skip=$datapos count=$datalen "of=${dumps}_$(printf "%06x" "$pos")_${datatype}.j2c" 2> /dev/null
;;
62706c6973743030*)
dd "if=$theicon" bs=1 skip=$datapos count=$datalen "of=${dumps}_$(printf "%06x" "$pos")_${datatype}.plist" 2> /dev/null
;;
esac
fi
((pos = datapos + datalen))
done
}
dumpicns () {
#1 = icns file
#2 = prefix for png, jpg, etc. extracts
dumpicnslist "$1" "$2" $(($(stat -f %z "$1") + 0)) 0
}
makeicon () {
#1 = source icon suite
#... = hex offset of each icon in the icon suite that you want to include
local theicon="$1"
shift
local theicondata=""
while (($#)); do
theoffset="$1"
shift
if [[ "${theoffset}" = "-" ]]; then
thetype="$1"
shift
thesize="$1"
shift
theicondata=${theicondata}$(printf "%s" "$thetype" | xxd -p)$(printf "%08x" "$thesize")$(xxd -p -l $((thesize - 8)) -c $((thesize - 8)) /dev/random)
else
theheader=$(xxd -p -s $((0x$theoffset)) -l 8 "$theicon")
thesize=$((0x${theheader:8}))
theicondata=${theicondata}$(xxd -p -s $((0x$theoffset)) -l $thesize -c $thesize "$theicon")
fi
done
printf "69636e73%08x%s" $((${#theicondata} / 2 + 8)) "$theicondata" | xxd -p -r
}
iconfromresource () {
local srcicon="$1"
local dsticon="$2"
DeRez "${srcicon}" -only "'icns' (-16455)" | perl -nE 'if (/^\t\$"([0-9A-F ]+)".*/) { print $1 }' | xxd -p -r > "$dsticon"
}
icontoresource () {
local srcicon="$1"
local dsticon="$2"
{
printf "data 'icns' (-16455) {\n"
xxd -p -c 16 "$srcicon" | perl -pE 's/(.*)/\$"\1"/'
printf "};\n"
} > "/tmp/iconrez.r"
Rez "/tmp/iconrez.r" -o "$dsticon"
}
checkiconcompatibility () {
#.VolumeIcon.icns requirements:
#- 10.4: icons in an icns file cannot be 300KB or greater otherwise the icns is not used. 1024x1024 icons are usually too large. Some 512x512 icons are too large (depends on png compressibility).
#- 10.5: The icns file cannot exceed 1MB-2B otherwise the icns is not used.
#- 10.6: no limits that I've encountered
#- 10.7: TOC must be ordered if it exists
#- 10.8: TOC must be ordered if it exists
#- 10.9 - 12.2: no limits that I've encountered
#- Startup Manager on old Macs like my MacPro3,1 require it32 icons (at least for the 128x128 size - I haven't checked what icon types can be used)
#- rEFInd doesn't do icons that have png or jpeg or ARGB data - it only does the RGB+8 icon types (it32 and it's smaller versions).
#- I added support for png and ARGB to my RefindPlus fork (plus old 8bit, 4bit, 1bit icons! I tested these by converting all icons in a Mac OS 9
# system file and ROM resources into icns files). I don't know where to get a jpeg 2000 library that can be ported to EFI to support icns files that have jpeg 2000 icons.
local disklist=""
local doingfolders=0
local copycommand="cp -p"
if [[ $1 == '-f' ]]; then
doingfolders=1
local copycommand="icontoresource"
disklist=$( find "$2" -type f -name "Icon"$'\r' -exec dirname {} \; )
elif [[ -n $1 ]]; then
disklist="$(getallmounteddisks -d | grep -E "$1")"
else
disklist="$(getallmounteddisks -d)"
fi
local foldernumber="1"
while [[ -e VolumeIcons"${foldernumber}" ]]; do
((foldernumber++))
done
IFS=$'\n'
for thepart in $(printf "%s" "$disklist"); do
local themount=""
local thedevice=""
local thevolume=""
local theicon=""
if ((doingfolders)); then
thedevice=""
themount="$thepart"
thevolume="$(basename "$themount")"
theicon="$themount/Icon"$'\r'
else
thedevice="${thepart%:*}"
themount="${thepart#*:}"
thevolume="$(basename "$themount")"
theicon="$themount/.VolumeIcon.icns"
fi
echo "#•••••$(dirname "$theicon")"
if [[ -f "$theicon" ]]; then
local thecount=""
while [[ -d "VolumeIcons${foldernumber}/$thevolume$thecount" ]]; do
((thecount++))
done
local thedir="VolumeIcons${foldernumber}/$thevolume$thecount"
mkdir -p "$thedir"
local theprefix="$thedir/$thevolume"
if ((doingfolders)); then
iconfromresource "${theicon}" "$theprefix.icns"
else
cp -p "${theicon}" "$theprefix.icns"
fi
icns2icns "${theprefix}.icns" "${theprefix}_after.icns" || echo "### Error icns2icns"
dumpicns "${theprefix}.icns" > "${theprefix}.txt" || echo "### Error dumpicns"
dumpicns "${theprefix}_after.icns" > "${theprefix}_after.txt" || echo "### Error dumpicns2"
it32=1
grep it32 "${theprefix}.txt" > /dev/null || it32=0
if (( it32 )); then
echo "# Found it32 in icon at $thepart"
local toccontents=""
toccontents="$(perl -ne 'while (<>) {if ((/^.{8}: 544f4320.*/ .. /^$/) && /^ /) { s/^ .{8}:(( \w{8})+).*\n/\1/; s/ (.{8}) .{8}/\1 /g; print "$_"; } } ' < "${theprefix}.txt")"
if [[ -n $toccontents ]]; then # has a TOC
local tocexpected=""
tocexpected="$(perl -ne 'while (<>) {if (!/^00000000/ && !/^\w{8}: 544f4320/ && !/^ / && !/^$/) { s/\w{8}: (\w{8}).*\n/\1/; print "$_ "; } } ' < "${theprefix}.txt")"
if [[ $toccontents != "$tocexpected" ]]; then
echo "#TOC was not sorted"
printf '%s "%s" "%s"' "$copycommand" "${theprefix}_after.icns" "$theicon" | perl -0777 -pE 's/\r/"\$'"'"'\\r'"'"'"/g'
echo
fi
fi
if [[ -z $device ]] || [[ "$(getvolumeproperty "$thedevice" FilesystemType)" != "apfs" ]]; then
# we only need to worry about icon sizes in partitions that are viewable in old macOS versions which are unable to mount apfs partitions
IFS=$'\n'
local iconoffset=""
local iconsize=""
local icontype=""
local includedicons=""
local excludedicons=0
local totalsize=0
local includedsizes=0
for theline in $(sed -nE '/^([0-9A-Fa-f]+): .{8} (.{8}) .{53} (....).*/s//iconoffset=\1;iconsize=$((0x\2));icontype="\3"/p' "${theprefix}.txt"); do
eval "$theline"
if [[ $icontype = "icns" ]]; then
totalsize=iconsize
elif [[ $icontype != "icns" ]] && (( iconsize >= 307200 )); then
echo "#icon at $iconoffset is too large"
((excludedicons++))
elif [[ $icontype != 'TOC ' ]]; then
includedicons="${includedicons} ${iconoffset}"
((includedsizes += iconsize))
fi
done
if (( totalsize > 1048574 )); then
printf "#icon file is too large by %x bytes" $((totalsize - 1048574))
if (( includedsizes > 1048574 )); then
if (( includedsizes == totalsize )); then
printf "\n"
else
printf " or %x bytes after remaking the icon\n" $((includedsizes - 1048574))
fi
else
printf " but remaking the icon is sufficient\n"
fi
((excludedicons++))
fi
if ((excludedicons)); then
echo 'bbedit "'"${theprefix}.txt"'"'
echo 'makeicon "'"${theprefix}.icns"'"'"${includedicons}"' > "'"${theprefix}_reduced.icns"'"'
printf '%s "%s" "%s"' "$copycommand" "${theprefix}_reduced.icns" "$theicon" | perl -0777 -pE 's/\r/"\$'"'"'\\r'"'"'"/g'
echo
fi
fi
else
it32after=1
grep it32 "${theprefix}_after.txt" > /dev/null || it32after=0
if (( it32after )); then
echo "#it32 added"
printf '%s "%s" "%s"' "$copycommand" "${theprefix}_after.icns" "$theicon" | perl -0777 -pE 's/\r/"\$'"'"'\\r'"'"'"/g'
echo
else
echo "#it32 didn't get added - try explicitly adding it"
icns2image "${theprefix}.icns" "${theprefix}.tiff" || echo "### Error icns2image"
image2icns "${theprefix}.tiff" "${theprefix}_after2.icns" || echo "### Error image2icns"
dumpicns "${theprefix}_after2.icns" > "${theprefix}_after2.txt" || echo "### Error dumpicns2"
printf '%s "%s" "%s"' "$copycommand" "${theprefix}_after2.icns" "$theicon" | perl -0777 -pE 's/\r/"\$'"'"'\\r'"'"'"/g'
echo
fi
fi
else
echo "# No icon at $thepart"
fi
done 2>&1
}
@joevt
Copy link
Author

joevt commented Dec 24, 2021

Download

curl -L https://gist.github.com/joevt/9fa524ebbef3db46842f14f33cf64ca5/raw -o ~/Downloads/VolumeIconUtil.sh
curl -L https://gist.github.com/joevt/6d7a0ede45106345a39bdfa0ac10ffd6/raw -o ~/Downloads/DiskUtil.sh

You need to get osxiconutils and update the aliases in the VolumeIconUtil.sh script for some features.

Install (temporarily, only for the current terminal window)

source ~/Downloads/VolumeIconUtil.sh
source ~/Downloads/DiskUtil.sh

Test

# Dump the list of icons in the Volume Icon of the root volume:
dumpicns /.VolumeIcon.icns

# Same as above and extract any png, jp2, or plist items into files with the prefix "macOS_" and list the results:
dumpicns /.VolumeIcon.icns macOS
ls macOS_*

# Make an icon using items (specified by hex offset) from the Volume Icon of the root volume and check the results:
makeicon /.VolumeIcon.icns 0000CBAF 0001CD57 > NewIcon.icns
dumpicns NewIcon.icns

# Mount all partitions and check the compatibility of all Volume Icons of all mounted disk partitions:
# - Icons are copied to a new VolumeIcon folder - the source Volume Icons are unchanged.
# - A new icon is created in the VolumeIcon folder with the items sorted. It will have a TOC and a it32.
# - If the new icon has changes then a command is output that can be used to replace the source Volume Icon.
# - The command will exclude items that are 300 KiB or larger (for Mac OS X 10.4 compatibility).
# - You may want to exclude other items to make the Volume Icon file less than 1 MiB (for Mac OS X 10.5 compatibility).
mountEFIpartitions 
mountPrebootPartitions
mountRecoveryPartitions
mountRecoveryHDpartitions 
checkiconcompatibility

Changes
May 1, 2023

  • dumpicns no longer reports syntax error when given a path to an icon file that doesn't exist.
  • checkiconcompatibility now accepts a path to a single disk for when you don't want to check all disks.

July 16, 2024

  • checkiconcompatibility now accepts a path to a folder to check folder icon files instead of volume icon files.
    Use it like this:
    checkiconcompatibility -f folderpath

@startergo
Copy link

In Mojave I get:

dumpicns /.VolumeIcon.icns
stat: /.VolumeIcon.icns: stat: No such file or directory
-bash: ((: 0 <  : syntax error: operand expected (error token is " ")
G5s-Mac-Pro:osxiconutils g5$ dumpicns /.VolumeIcon.icns macOS
stat: /.VolumeIcon.icns: stat: No such file or directory
-bash: ((: 0 <  : syntax error: operand expected (error token is " ")
G5s-Mac-Pro:osxiconutils g5$ mountPrebootPartitions
Password:
G5s-Mac-Pro:osxiconutils g5$ mountRecoveryPartitions
G5s-Mac-Pro:osxiconutils g5$ mountRecoveryHDpartitions
G5s-Mac-Pro:osxiconutils g5$ checkiconcompatibility
#•••••/Volumes/Monterey - Data/.VolumeIcon.icns
# No icon at disk4s1:/Volumes/Monterey - Data
#•••••/Volumes/Preboot/.VolumeIcon.icns
# No icon at disk4s2:/Volumes/Preboot
#•••••/Volumes/Recovery/.VolumeIcon.icns
# No icon at disk4s3:/Volumes/Recovery
#•••••/Volumes/Update/.VolumeIcon.icns
# No icon at disk4s5:/Volumes/Update
#•••••/Volumes/High Sierra/.VolumeIcon.icns
# No icon at disk5s1:/Volumes/High Sierra
#•••••/Volumes/Preboot1/.VolumeIcon.icns
# No icon at disk5s2:/Volumes/Preboot1
#•••••/Volumes/Recovery1/.VolumeIcon.icns
# No icon at disk5s3:/Volumes/Recovery1
#•••••/Volumes/WDC1TB/.VolumeIcon.icns
# No icon at disk6s2:/Volumes/WDC1TB
#•••••//.VolumeIcon.icns
# No icon at disk7s2:/
#•••••/Volumes/HFS+/.VolumeIcon.icns
# No icon at disk8s2:/Volumes/HFS+
#•••••/Volumes/BOOTCAMP/.VolumeIcon.icns
# No icon at disk8s3:/Volumes/BOOTCAMP
#•••••/Volumes/Backup/.VolumeIcon.icns
# No icon at disk8s4:/Volumes/Backup
#•••••/Volumes/Mavericks/.VolumeIcon.icns
# No icon at disk9s2:/Volumes/Mavericks
#•••••/Volumes/Recovery HD/.VolumeIcon.icns
# No icon at disk9s3:/Volumes/Recovery HD
G5s-Mac-Pro:osxiconutils g5$ dumpicns /.VolumeIcon.icns
stat: /.VolumeIcon.icns: stat: No such file or directory
-bash: ((: 0 <  : syntax error: operand expected (error token is " ")

@joevt
Copy link
Author

joevt commented May 1, 2023

  • Made a small fix so you should only see the stat: No such file or directory error when you pass a path to a icon file that doesn't exist. You won't see the syntax error message that is caused by the non-existing icon file.
  • Changed checkiconcompatibility so that it will accept a path to a single disk for when you don't want to check all disks.

@joevt
Copy link
Author

joevt commented Jul 17, 2024

  • Changed checkiconcompatibility so that it will accept a path to a folder to check folder icon files instead of volume icon files.
    Use it like this:
    checkiconcompatibility -f folderpath

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment