#!/usr/bin/env bash set -euo pipefail usage() { echo "Usage: $(basename "$0") [output.icns]" >&2 } gen-png() { local src="$1" local px="$2" local dpi="$3" local out="$4" # Resize longest side to , then pad to a square x with transparency. local tmp_resized tmp_resized="${out}.tmp.png" # Scale preserving aspect ratio; longest side becomes . sips -Z "$px" "$src" --out "$tmp_resized" > /dev/null # Pad to exact square dimensions centered on transparent background. sips --padToHeightWidth "$px" "$px" \ "$tmp_resized" --out "$out" > /dev/null rm -f "$tmp_resized" || true # Set DPI metadata. sips -s dpiHeight "$dpi" -s dpiWidth "$dpi" "$out" > /dev/null } get-image-max-dim() { local src="$1" local w h # Capture width/height; avoid exiting on failure due to set -e/pipefail. w=$(sips -g pixelWidth "$src" 2> /dev/null | awk '/pixelWidth/ {print $2}') || true h=$(sips -g pixelHeight "$src" 2> /dev/null | awk '/pixelHeight/ {print $2}') || true if [[ -z "$w" || -z "$h" ]]; then echo "Failed to read image dimensions with sips: $src" >&2 return 1 fi if ((w > h)); then echo "$w" else echo "$h" fi } verify-png() { local src="$1" local fmt fmt=$(sips -g format "$src" 2> /dev/null | awk '/format/ {print tolower($2)}') || true if [[ -z "$fmt" ]]; then echo "Unable to determine image format with sips: $src" >&2 return 1 fi if [[ "$fmt" != "png" ]]; then echo "Input must be a PNG image: $src" >&2 return 1 fi } make-iconset() { local src="$1" local dest_dir="$2" mkdir -p "$dest_dir" # Sizes to generate (@1x). @2x files are double the pixels. local sizes=(16 32 128 256 512) # Determine maximum source dimension to avoid upscaling. local src_max if ! src_max=$(get-image-max-dim "$src"); then return 1 fi local generated generated=0 for sz in "${sizes[@]}"; do # @1x local out1="${dest_dir}/icon_${sz}x${sz}.png" if ((sz <= src_max)); then gen-png "$src" "$sz" 72 "$out1" generated=$((generated + 1)) fi # @2x (double pixels, higher DPI) local dsz=$((sz * 2)) local out2="${dest_dir}/icon_${sz}x${sz}@2x.png" if ((dsz <= src_max)); then gen-png "$src" "$dsz" 144 "$out2" generated=$((generated + 1)) fi done if ((generated == 0)); then echo "Source image too small; no icon sizes generated: $src" >&2 return 1 fi } cleanup() { local path="$1" if [[ -z "$path" || ! -d "$path" ]]; then return 0 fi # Remove generated PNGs inside the temp directory find "$path" -type f -name '*.png' -delete 2> /dev/null || true # Remove generated `*.iconset` inside the temp directory find "$path" -type d -name '*.iconset' -delete 2> /dev/null || true # Attempt to remove the temp directory if now empty rmdir "$path" 2> /dev/null || true } main() { if [[ $# -lt 1 || $# -gt 2 ]]; then usage exit 1 fi local src="$1" local out="${2:-${src%.*}.icns}" if [[ ! -f "$src" ]]; then echo "Source file not found: $src" >&2 exit 1 fi if ! command -v sips > /dev/null 2>&1; then echo "This script requires 'sips' (macOS) but it wasn't found." >&2 exit 1 fi if ! command -v iconutil > /dev/null 2>&1; then echo "This script requires 'iconutil' (macOS) but it wasn't found." >&2 exit 1 fi local outdir outdir="$(dirname "$out")" mkdir -p "$outdir" # Create a temporary working directory tmp_root="$(mktemp -d 2> /dev/null || mktemp -d -t png2icns)" trap 'cleanup "$tmp_root"' EXIT # Verify input is PNG; we do not accept other formats. if ! verify-png "$src"; then exit 1 fi local iconset_dir iconset_dir="${tmp_root}/icon.iconset" make-iconset "$src" "$iconset_dir" # Build `.icns` from the generated iconset if ! iconutil -c icns -o "$out" "$iconset_dir" 2> /dev/null; then echo "Failed to create .icns with iconutil" >&2 exit 1 fi echo "Done. ICNS written to: $out" } main "$@"