feat(build): automate legacy *.icns icon creation (#10)
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 516 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 516 KiB |
BIN
Icons/Exports/EmacsLG1-iOS-Dark-1024x1024@2x.png
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
Icons/Exports/EmacsLG1-iOS-Default-1024x1024@2x.png
Normal file
|
After Width: | Height: | Size: 4.7 MiB |
BIN
Icons/Exports/EmacsLG1-macOS-Dark-1024x1024@2x.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
Icons/Exports/EmacsLG1-macOS-Default-1024x1024@2x.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 354 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 354 KiB |
BIN
Icons/Exports/EmacsLG2-iOS-Dark-1024x1024@2x.png
Normal file
|
After Width: | Height: | Size: 9.2 MiB |
BIN
Icons/Exports/EmacsLG2-iOS-Default-1024x1024@2x.png
Normal file
|
After Width: | Height: | Size: 9.0 MiB |
BIN
Icons/Exports/EmacsLG2-macOS-Dark-1024x1024@2x.png
Normal file
|
After Width: | Height: | Size: 6.7 MiB |
BIN
Icons/Exports/EmacsLG2-macOS-Default-1024x1024@2x.png
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 410 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 403 KiB |
BIN
Icons/Exports/EmacsLG3-iOS-Dark-1024x1024@2x.png
Normal file
|
After Width: | Height: | Size: 10 MiB |
BIN
Icons/Exports/EmacsLG3-iOS-Default-1024x1024@2x.png
Normal file
|
After Width: | Height: | Size: 11 MiB |
BIN
Icons/Exports/EmacsLG3-macOS-Dark-1024x1024@2x.png
Normal file
|
After Width: | Height: | Size: 7.9 MiB |
BIN
Icons/Exports/EmacsLG3-macOS-Default-1024x1024@2x.png
Normal file
|
After Width: | Height: | Size: 7.7 MiB |
@@ -1,27 +1,11 @@
|
||||
# EmacsLG Legacy *.icns Exports
|
||||
# EmacsLG PNG Exports
|
||||
|
||||
We have a `EmacsLG-Iconsets.afdesign` Affinity Designer document which contains
|
||||
high-res versions of all icon variants, and export configurations to export all
|
||||
icons at all required sizes to relevant `*.iconset` folders.
|
||||
PNG images here are exported from Icon Composer at 1024x1024@2x, meaning they're
|
||||
actually 2048x2048 at 144 DPI.
|
||||
|
||||
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
|
||||
|
||||
Within Affinity Designer, simply switch to the Export persona, and click the
|
||||
"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'
|
||||
```
|
||||
The `*-macOS-*` files are exported with Icon Composers' export platform set to
|
||||
"macOS pre-Tahoe". This effectively adds a transparent padding around the icon,
|
||||
so they become equally sized to Apple's own icons.
|
||||
|
||||
25
Makefile
@@ -1,11 +1,22 @@
|
||||
# Output directory
|
||||
RESOURCES_DIR := Resources
|
||||
|
||||
# Icon files and names
|
||||
ICON_FILES := $(shell find Icons/ -depth 1 -name '*.icon')
|
||||
ICON_NAMES := $(basename $(notdir $(ICON_FILES)))
|
||||
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
|
||||
all: $(RESOURCES_DIR)/Assets.car
|
||||
all: $(RESOURCES_DIR)/Assets.car $(ICNS_FILES)
|
||||
|
||||
$(RESOURCES_DIR)/Assets.car: $(ICON_FILES) $(ICON_SOURCES)
|
||||
mkdir -p "$(RESOURCES_DIR)"
|
||||
@@ -22,6 +33,16 @@ $(RESOURCES_DIR)/Assets.car: $(ICON_FILES) $(ICON_SOURCES)
|
||||
--platform macosx \
|
||||
--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
|
||||
clean:
|
||||
find "$(RESOURCES_DIR)" -type f -name 'Assets.car' -delete
|
||||
find "$(RESOURCES_DIR)" -type f -name '*.icns' -delete
|
||||
|
||||
177
bin/png2icns
Executable 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 "$@"
|
||||