feat(build): automate legacy *.icns icon creation (#10)

This commit is contained in:
2025-10-18 22:05:50 +01:00
committed by GitHub
parent 9e7c5ac3ae
commit 587aa4f013
82 changed files with 208 additions and 26 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

View File

@@ -1,27 +1,11 @@
# EmacsLG Legacy *.icns Exports # EmacsLG PNG Exports
We have a `EmacsLG-Iconsets.afdesign` Affinity Designer document which contains PNG images here are exported from Icon Composer at 1024x1024@2x, meaning they're
high-res versions of all icon variants, and export configurations to export all actually 2048x2048 at 144 DPI.
icons at all required sizes to relevant `*.iconset` folders.
I picked Affinity Designer cause I'm familiar with its export features. The `*-iOS-*` files are confusingly, for iOS and macOS Liquid Glass icon
creation.
## Exporting *.iconset directories The `*-macOS-*` files are exported with Icon Composers' export platform set to
"macOS pre-Tahoe". This effectively adds a transparent padding around the icon,
Within Affinity Designer, simply switch to the Export persona, and click the so they become equally sized to Apple's own icons.
"Export Slices" button. This will create all `*.iconset` directories with all
resolutions of images.
## Create *.icns files
The `*.icns` icon files are creates from the `*.iconset` directories using the
`iconutil` CLI tool:
```bash
iconutil -c icns 'EmacsLG1-Default.iconset'
iconutil -c icns 'EmacsLG1-Dark.iconset'
iconutil -c icns 'EmacsLG2-Default.iconset'
iconutil -c icns 'EmacsLG2-Dark.iconset'
iconutil -c icns 'EmacsLG3-Default.iconset'
iconutil -c icns 'EmacsLG3-Dark.iconset'
```

View File

@@ -1,11 +1,22 @@
# Output directory
RESOURCES_DIR := Resources RESOURCES_DIR := Resources
# Icon files and names
ICON_FILES := $(shell find Icons/ -depth 1 -name '*.icon') ICON_FILES := $(shell find Icons/ -depth 1 -name '*.icon')
ICON_NAMES := $(basename $(notdir $(ICON_FILES)))
ICON_SOURCES := $(shell find $(ICON_FILES) -type f) ICON_SOURCES := $(shell find $(ICON_FILES) -type f)
# *.icns files
ICNS_VARIANTS := \
Default \
Dark
ICNS_FILES := $(foreach \
icon,$(ICON_NAMES), \
$(foreach variant,$(ICNS_VARIANTS), \
$(RESOURCES_DIR)/$(icon)-$(variant).icns \
))
.PHONY: all .PHONY: all
all: $(RESOURCES_DIR)/Assets.car all: $(RESOURCES_DIR)/Assets.car $(ICNS_FILES)
$(RESOURCES_DIR)/Assets.car: $(ICON_FILES) $(ICON_SOURCES) $(RESOURCES_DIR)/Assets.car: $(ICON_FILES) $(ICON_SOURCES)
mkdir -p "$(RESOURCES_DIR)" mkdir -p "$(RESOURCES_DIR)"
@@ -22,6 +33,16 @@ $(RESOURCES_DIR)/Assets.car: $(ICON_FILES) $(ICON_SOURCES)
--platform macosx \ --platform macosx \
--minimum-deployment-target 11.0 --minimum-deployment-target 11.0
# *.icns files generation rule
define ICNS_RULE
$(RESOURCES_DIR)/%-$(1).icns: Icons/Exports/%-macOS-$(1)-1024x1024@2x.png
@mkdir -p $(RESOURCES_DIR)
bin/png2icns "$$<" "$$@"
endef
$(foreach variant,$(ICNS_VARIANTS),$(eval $(call ICNS_RULE,$(variant))))
.PHONY: clean .PHONY: clean
clean: clean:
find "$(RESOURCES_DIR)" -type f -name 'Assets.car' -delete find "$(RESOURCES_DIR)" -type f -name 'Assets.car' -delete
find "$(RESOURCES_DIR)" -type f -name '*.icns' -delete

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

177
bin/png2icns Executable file
View File

@@ -0,0 +1,177 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $(basename "$0") <source.png> [output.icns]" >&2
}
gen-png() {
local src="$1"
local px="$2"
local dpi="$3"
local out="$4"
# Resize longest side to <px>, then pad to a square <px>x<px> with transparency.
local tmp_resized
tmp_resized="${out}.tmp.png"
# Scale preserving aspect ratio; longest side becomes <px>.
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 "$@"