nixpkgs/pkgs/build-support/setup-hooks/desktop-to-darwin-bundle.sh
Karolis Stasaitis 3b52cd4c1b desktopToDarwinBundle: fix 16x, 32x app icons
Changes the script to produce rgb+mask images for 16x and 32x icons
instead of png.

Using icns files containing 16x and 32x png images for app bundles
results in Finder misrendering icons for these sizes (even though the
icns files are rendered correctly when viewed by themselves)
2024-11-23 15:55:09 +01:00

264 lines
9.6 KiB
Bash

# shellcheck shell=bash
fixupOutputHooks+=('convertDesktopFiles $prefix')
# Get a param out of a desktop file. First parameter is the file and the second
# is the key who's value we should fetch.
getDesktopParam() {
local file="$1"
local key="$2"
local line k v
while read -r line; do
if [[ "$line" = *=* ]]; then
k="${line%%=*}"
v="${line#*=}"
if [[ "$k" = "$key" ]]; then
echo "$v"
return
fi
fi
done < "$file"
return 1
}
# Convert a freedesktop.org icon theme for a given app to a .icns file. When possible, missing
# icons are synthesized from SVG or rescaled from existing ones (when within the size threshold).
convertIconTheme() {
local -r out=$1
local -r sharePath=$2
local -r iconName=$3
local -r theme=${4:-hicolor}
# Sizes based on archived Apple documentation:
# https://developer.apple.com/design/human-interface-guidelines/macos/icons-and-images/app-icon#app-icon-sizes
local -ra iconSizes=(16 32 128 256 512)
local -ra scales=([1]="" [2]="@2")
# Based loosely on the algorithm at:
# https://specifications.freedesktop.org/icon-theme-spec/latest/#icon_lookup
# Assumes threshold = 2 for ease of implementation.
function findIcon() {
local -r iconSize=$1
local -r scale=$2
local scaleSuffix=${scales[$scale]}
local exactSize=${iconSize}x${iconSize}${scaleSuffix}
local -a validSizes=(
${exactSize}
$((iconSize + 1))x$((iconSize + 1))${scaleSuffix}
$((iconSize + 2))x$((iconSize + 2))${scaleSuffix}
$((iconSize - 1))x$((iconSize - 1))${scaleSuffix}
$((iconSize - 2))x$((iconSize - 2))${scaleSuffix}
)
local fallbackIcon=
for iconIndex in "${!candidateIcons[@]}"; do
for maybeSize in "${validSizes[@]}"; do
icon=${candidateIcons[$iconIndex]}
if [[ $icon = */$maybeSize/* ]]; then
if [[ $maybeSize = $exactSize ]]; then
echo "fixed $icon"
return 0
else
echo "threshold $icon"
return 0
fi
elif [[ -a $icon && -z "$fallbackIcon" ]]; then
fallbackIcon="$icon"
fi
done
done
if [[ -n "$fallbackIcon" ]]; then
echo "fallback $fallbackIcon"
return 0
fi
echo "scalable"
}
function resizeIcon() {
local -r in=$1
local -r out=$2
local -r iconSize=$3
local -r scale=$4
local density=$((72 * scale))x$((72 * scale))
local dim=$((iconSize * scale))
echo "desktopToDarwinBundle: resizing icon $in to $out, size $dim" >&2
magick convert -scale "${dim}x${dim}" -density "$density" -units PixelsPerInch "$in" "$out"
convertIfUnsupportedIcon "$out" "$iconSize" "$scale"
}
function synthesizeIcon() {
local -r in=$1
local -r out=$2
local -r iconSize=$3
local -r scale=$4
if [[ $in != '-' ]]; then
local density=$((72 * scale))x$((72 * scale))
local dim=$((iconSize * scale))
echo "desktopToDarwinBundle: rasterizing svg $in to $out, size $dim" >&2
rsvg-convert --keep-aspect-ratio --width "$dim" --height "$dim" "$in" --output "$out"
magick convert -density "$density" -units PixelsPerInch "$out" "$out"
convertIfUnsupportedIcon "$out" "$iconSize" "$scale"
else
return 1
fi
}
# macOS does not correctly display 16x and 32x png icons on app bundles
# they need to be converted to rgb+mask (argb is supported only from macOS 11)
function convertIfUnsupportedIcon() {
local -r in=$1
local -r iconSize=$2
local -r scale=$3
local -r out=${in%.png}.rgb
if [[ ($scale -eq 1) && ($iconSize -eq 32 || $iconSize -eq 16) ]]; then
echo "desktopToDarwinBundle: converting ${iconSize}x icon to rgb" >&2
icnsutil convert "$out" "$in"
rm "$in"
fi
}
function getIcons() {
local -r sharePath=$1
local -r iconname=$2
local -r theme=$3
local -r resultdir=$(mktemp -d)
local -ar candidateIcons=(
"${sharePath}/icons/${theme}/"*"/${iconname}.png"
"${sharePath}/icons/${theme}/"*"/${iconname}.xpm"
)
local -a scalableIcon=("${sharePath}/icons/${theme}/scalable/${iconname}.svg"*)
if [[ ${#scalableIcon[@]} = 0 ]]; then
scalableIcon=('-')
fi
# Tri-state variable, NONE means no icons have been found, an empty
# icns file will be generated, not sure that's necessary because macOS
# will default to a generic icon if no icon can be found.
#
# OTHER means an appropriate icon was found.
#
# Any other value is a path to an icon file that isn't scalable or
# within the threshold. This is used as a fallback in case no better
# icon can be found and will be scaled as much as
# necessary to result in appropriate icon sizes.
local foundIcon=NONE
for iconSize in "${iconSizes[@]}"; do
for scale in "${!scales[@]}"; do
local iconResult=$(findIcon $iconSize $scale)
local type=${iconResult%% *}
local icon=${iconResult#* }
local scaleSuffix=${scales[$scale]}
local result=${resultdir}/${iconSize}x${iconSize}${scales[$scale]}${scaleSuffix:+x}.png
echo "desktopToDarwinBundle: using $type icon $icon for size $iconSize$scaleSuffix" >&2
case $type in
fixed)
local density=$((72 * scale))x$((72 * scale))
magick convert -density "$density" -units PixelsPerInch "$icon" "$result"
convertIfUnsupportedIcon "$result" "$iconSize" "$scale"
foundIcon=OTHER
;;
threshold)
# Synthesize an icon of the exact size if a scalable icon is available
# instead of scaling one and ending up with a fuzzy icon.
if ! synthesizeIcon "${scalableIcon[0]}" "$result" "$iconSize" "$scale"; then
resizeIcon "$icon" "$result" "$iconSize" "$scale"
fi
foundIcon=OTHER
;;
scalable)
synthesizeIcon "${scalableIcon[0]}" "$result" "$iconSize" "$scale" || true
foundIcon=OTHER
;;
fallback)
# Use the largest size available to scale to
# appropriate sizes.
if [[ $foundIcon != OTHER ]]; then
foundIcon=$icon
fi
;;
*)
;;
esac
done
done
if [[ $foundIcon != NONE && $foundIcon != OTHER ]]; then
# Ideally we'd only resize to whatever the closest sizes are,
# starting from whatever icon sizes are available.
for iconSize in 16 32 128 256 512; do
local result=${resultdir}/${iconSize}x${iconSize}.png
resizeIcon "$foundIcon" "$result" "$iconSize" 1
done
fi
echo "$resultdir"
}
iconsdir=$(getIcons "$sharePath" "apps/${iconName}" "$theme")
if [[ -n "$(ls -A1 "$iconsdir")" ]]; then
icnsutil compose --toc "$out/${iconName}.icns" "$iconsdir/"*
else
echo "Warning: no icons were found. Creating an empty icon for ${iconName}.icns."
touch "$out/${iconName}.icns"
fi
}
processExecFieldCodes() {
local -r file=$1
local -r execRaw=$(getDesktopParam "${file}" "Exec")
local -r execNoK="${execRaw/\%k/${file}}"
local -r execNoKC="${execNoK/\%c/$(getDesktopParam "${file}" "Name")}"
local -r icon=$(getDesktopParam "${file}" "Icon")
local -r execNoKCI="${execNoKC/\%i/${icon:+--icon }${icon}}"
local -r execNoKCIfu="${execNoKCI/ \%[fu]/}"
local -r exec="${execNoKCIfu/ \%[FU]/}"
if [[ "$exec" != "$execRaw" ]]; then
echo 1>&2 "desktopToDarwinBundle: Application bundles do not understand desktop entry field codes. Changed '$execRaw' to '$exec'."
fi
echo "$exec"
}
# For a given .desktop file, generate a darwin '.app' bundle for it.
convertDesktopFile() {
local -r file=$1
local -r sharePath=$(dirname "$(dirname "$file")")
local -r name=$(getDesktopParam "${file}" "Name")
local -r macOSExec=$(getDesktopParam "${file}" "X-macOS-Exec")
if [[ "$macOSExec" ]]; then
local -r exec="$macOSExec"
else
local -r exec=$(processExecFieldCodes "${file}")
fi
local -r iconName=$(getDesktopParam "${file}" "Icon")
local -r squircle=$(getDesktopParam "${file}" "X-macOS-SquircleIcon")
mkdir -p "${!outputBin}/Applications/${name}.app/Contents/MacOS"
mkdir -p "${!outputBin}/Applications/${name}.app/Contents/Resources"
convertIconTheme "${!outputBin}/Applications/${name}.app/Contents/Resources" "$sharePath" "$iconName"
write-darwin-bundle "${!outputBin}" "$name" "$exec" "$iconName" "$squircle"
}
convertDesktopFiles() {
local dir="$1/share/applications/"
if [ -d "${dir}" ]; then
for desktopFile in $(find "$dir" -iname "*.desktop"); do
convertDesktopFile "$desktopFile";
done
fi
}