From 30a09ae9ac11c659e71c34ec2728608a5a6dee3f Mon Sep 17 00:00:00 2001 From: Randy Eckenrode Date: Sat, 19 Feb 2022 23:30:16 -0500 Subject: desktopToDarwinBundle: fix squircle icons - Convert icons to a single .icns file; and - Provide an opt-out via X-macOS-Squircle in the desktop item to override the squircle behavior when the source icons look bad when converted automatically. --- .../setup-hooks/desktop-to-darwin-bundle.sh | 148 ++++++++++++++++++--- 1 file changed, 132 insertions(+), 16 deletions(-) (limited to 'pkgs/build-support/setup-hooks/desktop-to-darwin-bundle.sh') diff --git a/pkgs/build-support/setup-hooks/desktop-to-darwin-bundle.sh b/pkgs/build-support/setup-hooks/desktop-to-darwin-bundle.sh index d1175d3a5f9b9..480bcde6c1c29 100644 --- a/pkgs/build-support/setup-hooks/desktop-to-darwin-bundle.sh +++ b/pkgs/build-support/setup-hooks/desktop-to-darwin-bundle.sh @@ -10,31 +10,147 @@ getDesktopParam() { awk -F "=" "/${pattern}/ {print \$2}" "${file}" } +# 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} + + 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/icon-theme-spec-latest.html#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} + $(expr $iconSize + 1)x$(expr $iconSize + 1)${scaleSuffix} + $(expr $iconSize + 2)x$(expr $iconSize + 2)${scaleSuffix} + $(expr $iconSize - 1)x$(expr $iconSize - 1)${scaleSuffix} + $(expr $iconSize - 2)x$(expr $iconSize - 2)${scaleSuffix} + ) + + for iconIndex in "${!candidateIcons[@]}"; do + for maybeSize in "${validSizes[@]}"; do + icon=${candidateIcons[$iconIndex]} + if [[ $icon = */$maybeSize/* ]]; then + if [[ $maybeSize = $exactSize ]]; then + echo "fixed $icon" + else + echo "threshold $icon" + fi + return 0 + fi + done + done + echo "scalable" + } + + function resizeIcon() { + local -r in=$1 + local -r out=$2 + local -r iconSize=$3 + local -r scale=$4 + + local density=$(expr 72 \* $scale)x$(expr 72 \* $scale) + local dim=$(expr $iconSize \* $scale) + + magick convert -scale "${dim}x${dim}" -density "$density" -units PixelsPerInch "$in" "$out" + } + + function synthesizeIcon() { + local -r in=$1 + local -r out=$2 + local -r iconSize=$3 + local -r scale=$4 + + if [[ $in != '-' ]]; then + local density=$(expr 72 \* $scale)x$(expr 72 \* $scale) + local dim=$(expr $iconSize \* $scale) + rsvg-convert --keep-aspect-ratio --width "$dim" --height "$dim" "$in" --output "$out" + magick convert -density "$density" -units PixelsPerInch "$out" "$out" + else + return 1 + 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 + + 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 + case $type in + fixed) + local density=$(expr 72 \* $scale)x$(expr 72 \* $scale) + magick convert -density "$density" -units PixelsPerInch "$icon" "$result" + ;; + 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 + ;; + scalable) + synthesizeIcon "${scalableIcon[0]}" "$result" "$iconSize" "$scale" || true + ;; + esac + done + done + echo "$resultdir" + } + + iconsdir=$(getIcons "$sharePath" "apps/${iconName}" "$theme") + if [[ ! -z "$(ls -1 "$iconsdir/"*)" ]]; then + icnsutil compose "$out/${iconName}.icns" "$iconsdir/"* + else + echo "Warning: no icons were found. Creating an empty icon for ${iconName}.icns." + touch "$out/${iconName}.icns" + fi +} + # For a given .desktop file, generate a darwin '.app' bundle for it. convertDesktopFile() { - local -r file="$1" + local -r file=$1 + local -r sharePath=$(dirname "$(dirname "$file")") local -r name=$(getDesktopParam "${file}" "^Name") local -r exec=$(getDesktopParam "${file}" "Exec") - local -r iconName=$(getDesktopParam "${file}" "Icon") - local -r iconFiles=$(find "$out/share/icons/" -name "${iconName}.*" 2>/dev/null); - local -r pixMaps=$(find "$out/share/pixmaps/" -name "${iconName}.xpm" 2>/dev/null); + local -r iconName=$(getDesktopParam "${file}" "^Icon") + local -r squircle=$(getDesktopParam "${file}" "X-macOS-SquircleIcon") mkdir -p "$out/Applications/${name}.app/Contents/MacOS" mkdir -p "$out/Applications/${name}.app/Contents/Resources" - local i=0; - for icon in $iconFiles; do - ln -s "$icon" "$out/Applications/${name}.app/Contents/Resources/$i-$(basename "$icon")" - (( i +=1 )); - done - - for pixmap in $pixMaps; do - local newIconName="$i-$(basename "$pixmap")"; - convert "$pixmap" "$out/Applications/${name}.app/Contents/Resources/${newIconName%.xpm}.png" - (( i +=1 )); - done + convertIconTheme "$out/Applications/${name}.app/Contents/Resources" "$sharePath" "$iconName" - write-darwin-bundle "$out" "$name" "$exec" + write-darwin-bundle "$out" "$name" "$exec" "$iconName" "$squircle" } convertDesktopFiles() { -- cgit 1.4.1