Skip to content

Instantly share code, notes, and snippets.

@joevt
Last active March 10, 2024 08:38
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 May 1, 2023
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="$(xxd -s $pos -l 32 -p -c 32 "$theicon")"
local datatypehex="${blob:0:8}"
local 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 "${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))
printf "%s" "$posstring"
if [[ $datatype = "TOC " ]]; then
printf "${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 "${blob:0:16}" | xxd -r -p | xxd -g 4 -l 32 -c 32 | sed -E 's/^[^:]*//'
dumpicnslist "$theicon" "${dumps}" $((datapos+datalen)) $datapos
else
printf "${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
#echo
((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 "$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
}
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=""
if [[ -n $1 ]]; then
disklist="$(getallmounteddisks -d | egrep "$1")"
else
disklist="$(getallmounteddisks -d)"
fi
local foldernumber="1"
while [[ -e VolumeIcons"${foldernumber}" ]]; do
((foldernumber++))
done
IFS=$'\n'
for thepart in $(echo "$disklist"); do
local themount="${thepart#*:}"
local thedevice="${thepart%:*}"
local thevolume="$(basename "$themount")"
local theicon="$themount/.VolumeIcon.icns"
echo "#•••••$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"
cp -p "${theicon}" "$theprefix.icns"
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 at icon at $thepart"
local 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="$(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"
echo 'cp -p "'"${theprefix}_after.icns"'" "'"$theicon"'"'
fi
fi
if [[ "$(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"'"'
echo 'cp -p "'"${theprefix}_reduced.icns"'" "'"$theicon"'"'
fi
fi
else
it32after=1
grep it32 "${theprefix}_after.txt" > /dev/null || it32after=0
if (( it32after )); then
echo "#it32 added"
echo 'cp -p "'"${theprefix}_after.icns"'" "'"$theicon"'"'
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"
echo 'cp -p "'"${theprefix}_after2.icns"'" "'"$theicon"'"'
fi
fi
else
echo "# No icon at $thepart"
fi
done 2>&1
}
@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.

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