chore(docs/img): automate generating logo and preview images (#11)
73
Makefile
@@ -1,22 +1,34 @@
|
||||
RESOURCES_DIR := Resources
|
||||
IMG_DIR := img
|
||||
|
||||
# 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)
|
||||
ICON_FILES := $(sort $(shell find Icons/ -depth 1 -name '*.icon'))
|
||||
ICON_NAMES := $(sort $(basename $(notdir $(ICON_FILES))))
|
||||
ICON_SOURCES := $(sort $(shell find $(ICON_FILES) -type f))
|
||||
ICON_VARIANTS := Default Dark
|
||||
|
||||
# *.icns files
|
||||
ICNS_VARIANTS := \
|
||||
Default \
|
||||
Dark
|
||||
ICNS_FILES := $(foreach \
|
||||
icon,$(ICON_NAMES), \
|
||||
$(foreach variant,$(ICNS_VARIANTS), \
|
||||
$(foreach variant,$(ICON_VARIANTS), \
|
||||
$(RESOURCES_DIR)/$(icon)-$(variant).icns \
|
||||
))
|
||||
) \
|
||||
)
|
||||
|
||||
#
|
||||
# Make all targets
|
||||
#
|
||||
|
||||
.PHONY: all
|
||||
all: $(RESOURCES_DIR)/Assets.car $(ICNS_FILES)
|
||||
all: \
|
||||
$(RESOURCES_DIR)/Assets.car \
|
||||
$(ICNS_FILES) \
|
||||
$(foreach icon,$(ICON_NAMES),$(IMG_DIR)/$(icon)-preview.png) \
|
||||
$(IMG_DIR)/logo.png
|
||||
|
||||
#
|
||||
# Resources/Assets.car generation
|
||||
#
|
||||
|
||||
$(RESOURCES_DIR)/Assets.car: $(ICON_FILES) $(ICON_SOURCES)
|
||||
mkdir -p "$(RESOURCES_DIR)"
|
||||
@@ -33,14 +45,53 @@ $(RESOURCES_DIR)/Assets.car: $(ICON_FILES) $(ICON_SOURCES)
|
||||
--platform macosx \
|
||||
--minimum-deployment-target 11.0
|
||||
|
||||
# *.icns files generation rule
|
||||
#
|
||||
# Resources/*.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))))
|
||||
$(foreach variant,$(ICON_VARIANTS),$(eval $(call ICNS_RULE,$(variant))))
|
||||
|
||||
#
|
||||
# img/*.png generation
|
||||
#
|
||||
|
||||
img/logo.png: \
|
||||
$(foreach icon,$(ICON_NAMES), \
|
||||
Icons/Exports/$(icon)-iOS-Default-1024x1024@2x.png \
|
||||
)
|
||||
mkdir -p $(IMG_DIR)
|
||||
bin/pngs2collage \
|
||||
--output "$@" \
|
||||
--max-width 512 \
|
||||
--max-height 512 \
|
||||
--gap 64 \
|
||||
--border 0 \
|
||||
--max-columns 99 \
|
||||
$^
|
||||
|
||||
img/%-preview.png: \
|
||||
$(foreach variant,$(ICON_VARIANTS), \
|
||||
Icons/Exports/%-iOS-$(variant)-1024x1024@2x.png \
|
||||
)
|
||||
mkdir -p $(IMG_DIR)
|
||||
bin/pngs2collage \
|
||||
--output "$@" \
|
||||
--max-width 512 \
|
||||
--max-height 512 \
|
||||
--gap 64 \
|
||||
--border 0 \
|
||||
--max-columns 99 \
|
||||
$^
|
||||
|
||||
#
|
||||
# Clean up
|
||||
#
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
|
||||
@@ -62,21 +62,21 @@ be left alone.
|
||||
EmacsLG1 simplifies and changes the outer shape of the default icon to comply
|
||||
with the shape of Liquid Glass icons.
|
||||
|
||||

|
||||

|
||||
|
||||
### EmacsLG2
|
||||
|
||||
EmacsLG2 stays as close to the original round icon as possible, while giving it
|
||||
that Liquid Glass flair.
|
||||
|
||||

|
||||

|
||||
|
||||
### EmacsLG3
|
||||
|
||||
EmacsLG3 is the biggest divergence from Emacs' default icon, and is inspired by
|
||||
the changes Apple has made to some of its own icons in macOS 26.
|
||||
|
||||

|
||||

|
||||
|
||||
## License
|
||||
|
||||
|
||||
461
bin/pngs2collage
Executable file
@@ -0,0 +1,461 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat >&2 << EOF
|
||||
Usage: $(basename "$0") [OPTIONS] <input1.png> <input2.png> [input3.png ...]
|
||||
|
||||
Create a grid collage from multiple PNG images.
|
||||
|
||||
OPTIONS:
|
||||
-o, --output <file> Output filename (default: collage.png)
|
||||
--max-width <pixels> Maximum width for grid cells (default: 1024)
|
||||
--max-height <pixels> Maximum height for grid cells (default: 1024)
|
||||
--gap <pixels> Gap between images (default: 0)
|
||||
--border <pixels> Border around entire collage (default: 0)
|
||||
--max-columns <count> Maximum columns in grid (default: auto)
|
||||
--dpi <value> DPI for output image (default: 72)
|
||||
-h, --help Show this help message
|
||||
|
||||
ARGUMENTS:
|
||||
At least 2 input PNG files are required.
|
||||
|
||||
EXAMPLES:
|
||||
$(basename "$0") img1.png img2.png
|
||||
$(basename "$0") -o output.png --gap 10 img1.png img2.png img3.png
|
||||
$(basename "$0") --output result.png --max-columns 3 *.png
|
||||
EOF
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
get-image-dimensions() {
|
||||
local src="$1"
|
||||
local w h
|
||||
|
||||
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
|
||||
|
||||
echo "$w $h"
|
||||
}
|
||||
|
||||
calculate-cell-size() {
|
||||
local max_w="$1"
|
||||
local max_h="$2"
|
||||
shift 2
|
||||
local images=("$@")
|
||||
|
||||
local cell_w=0
|
||||
local cell_h=0
|
||||
local any_exceeds=0
|
||||
|
||||
for img in "${images[@]}"; do
|
||||
local dims
|
||||
if ! dims=$(get-image-dimensions "$img"); then
|
||||
return 1
|
||||
fi
|
||||
local w h
|
||||
read -r w h <<< "$dims"
|
||||
|
||||
if ((w > max_w || h > max_h)); then
|
||||
any_exceeds=1
|
||||
fi
|
||||
|
||||
if ((w > cell_w)); then
|
||||
cell_w=$w
|
||||
fi
|
||||
if ((h > cell_h)); then
|
||||
cell_h=$h
|
||||
fi
|
||||
done
|
||||
|
||||
# If any image exceeds max dimensions, use max dimensions as cell size
|
||||
if ((any_exceeds)); then
|
||||
if ((max_w > max_h)); then
|
||||
echo "$max_w $max_w"
|
||||
else
|
||||
echo "$max_h $max_h"
|
||||
fi
|
||||
else
|
||||
# Use largest dimension from all images to make square cells
|
||||
if ((cell_w > cell_h)); then
|
||||
echo "$cell_w $cell_w"
|
||||
else
|
||||
echo "$cell_h $cell_h"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
calculate-padding() {
|
||||
local row="$1"
|
||||
local col="$2"
|
||||
local total_rows="$3"
|
||||
local total_cols="$4"
|
||||
local gap="$5"
|
||||
local border="$6"
|
||||
|
||||
local top bottom left right
|
||||
local half_gap
|
||||
|
||||
# Calculate half gap (rounded for odd gaps)
|
||||
half_gap=$(awk "BEGIN {print int($gap / 2)}")
|
||||
|
||||
# Top padding
|
||||
if ((row == 0)); then
|
||||
top=$border
|
||||
else
|
||||
top=$half_gap
|
||||
fi
|
||||
|
||||
# Bottom padding
|
||||
if ((row == total_rows - 1)); then
|
||||
bottom=$border
|
||||
else
|
||||
bottom=$half_gap
|
||||
fi
|
||||
|
||||
# Left padding
|
||||
if ((col == 0)); then
|
||||
left=$border
|
||||
else
|
||||
left=$half_gap
|
||||
fi
|
||||
|
||||
# Right padding
|
||||
if ((col == total_cols - 1)); then
|
||||
right=$border
|
||||
else
|
||||
right=$half_gap
|
||||
fi
|
||||
|
||||
echo "$top $bottom $left $right"
|
||||
}
|
||||
|
||||
prepare-image() {
|
||||
local src="$1"
|
||||
local cell_size="$2"
|
||||
local top="$3"
|
||||
local bottom="$4"
|
||||
local left="$5"
|
||||
local right="$6"
|
||||
local out="$7"
|
||||
|
||||
local tmp_resized tmp_padded
|
||||
tmp_resized="${out}.tmp.resized.png"
|
||||
tmp_padded="${out}.tmp.padded.png"
|
||||
|
||||
# Scale preserving aspect ratio; fit within cell size
|
||||
sips -Z "$cell_size" "$src" --out "$tmp_resized" > /dev/null
|
||||
|
||||
# Pad to exact square dimensions centered on transparent background
|
||||
sips --padToHeightWidth "$cell_size" "$cell_size" \
|
||||
"$tmp_resized" --out "$tmp_padded" > /dev/null
|
||||
|
||||
# Apply asymmetric padding using ImageMagick
|
||||
# Create a transparent canvas and composite the image at the correct position
|
||||
local final_width final_height
|
||||
final_width=$((cell_size + left + right))
|
||||
final_height=$((cell_size + top + bottom))
|
||||
|
||||
# Create transparent canvas of final size, then composite the prepared
|
||||
# image at position (left, top) to create the exact padding we want
|
||||
if ! magick -size "${final_width}x${final_height}" xc:none \
|
||||
"$tmp_padded" -geometry "+${left}+${top}" -composite \
|
||||
"$out" 2> /dev/null; then
|
||||
echo "Failed to apply padding to image: $src" >&2
|
||||
rm -f "$tmp_resized" "$tmp_padded" || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
rm -f "$tmp_resized" "$tmp_padded" || true
|
||||
}
|
||||
|
||||
calculate-grid-dimensions() {
|
||||
local count="$1"
|
||||
local max_cols="$2"
|
||||
|
||||
local cols rows
|
||||
|
||||
if [[ "$max_cols" == "auto" ]]; then
|
||||
# Calculate square-ish grid
|
||||
cols=$(awk "BEGIN {print int(sqrt($count) + 0.999)}")
|
||||
else
|
||||
if ((max_cols > count)); then
|
||||
cols=$count
|
||||
else
|
||||
cols=$max_cols
|
||||
fi
|
||||
fi
|
||||
|
||||
rows=$(awk "BEGIN {print int(($count + $cols - 1) / $cols)}")
|
||||
|
||||
echo "$cols $rows"
|
||||
}
|
||||
|
||||
create-collage() {
|
||||
local cols="$1"
|
||||
local rows="$2"
|
||||
local dpi="$3"
|
||||
local out="$4"
|
||||
shift 4
|
||||
local images=("$@")
|
||||
|
||||
# Build ImageMagick montage command
|
||||
# montage combines images in a grid with zero spacing
|
||||
# (padding is already applied to individual images)
|
||||
local tile_arg="${cols}x${rows}"
|
||||
|
||||
# Create collage with transparent background and zero spacing
|
||||
if ! magick montage "${images[@]}" \
|
||||
-tile "$tile_arg" \
|
||||
-geometry "+0+0" \
|
||||
-background "none" \
|
||||
"$out" 2> /dev/null; then
|
||||
echo "Failed to create collage with ImageMagick" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Set DPI metadata
|
||||
sips -s dpiHeight "$dpi" -s dpiWidth "$dpi" "$out" > /dev/null
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
# Attempt to remove the temp directory if now empty
|
||||
rmdir "$path" 2> /dev/null || true
|
||||
}
|
||||
|
||||
parse-args() {
|
||||
local max_width=1024
|
||||
local max_height=1024
|
||||
local gap=0
|
||||
local border=0
|
||||
local max_columns="auto"
|
||||
local dpi=72
|
||||
local output_file=""
|
||||
local -a input_files=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
-o | --output)
|
||||
output_file="$2"
|
||||
shift 2
|
||||
;;
|
||||
--max-width)
|
||||
max_width="$2"
|
||||
shift 2
|
||||
;;
|
||||
--max-height)
|
||||
max_height="$2"
|
||||
shift 2
|
||||
;;
|
||||
--gap)
|
||||
gap="$2"
|
||||
shift 2
|
||||
;;
|
||||
--border)
|
||||
border="$2"
|
||||
shift 2
|
||||
;;
|
||||
--max-columns)
|
||||
max_columns="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dpi)
|
||||
dpi="$2"
|
||||
shift 2
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
input_files+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Export parsed values for main to access
|
||||
export PARSED_MAX_WIDTH="$max_width"
|
||||
export PARSED_MAX_HEIGHT="$max_height"
|
||||
export PARSED_GAP="$gap"
|
||||
export PARSED_BORDER="$border"
|
||||
export PARSED_MAX_COLUMNS="$max_columns"
|
||||
export PARSED_DPI="$dpi"
|
||||
export PARSED_OUTPUT="$output_file"
|
||||
export PARSED_INPUT_COUNT="${#input_files[@]}"
|
||||
|
||||
# Export input files as a string (will be parsed in main)
|
||||
local i
|
||||
for i in "${!input_files[@]}"; do
|
||||
export "PARSED_INPUT_$i=${input_files[$i]}"
|
||||
done
|
||||
}
|
||||
|
||||
main() {
|
||||
if [[ $# -lt 2 ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse arguments
|
||||
parse-args "$@"
|
||||
|
||||
# Reconstruct input files array from exports
|
||||
local -a all_files=()
|
||||
local i
|
||||
for ((i = 0; i < PARSED_INPUT_COUNT; i++)); do
|
||||
local varname="PARSED_INPUT_$i"
|
||||
all_files+=("${!varname}")
|
||||
done
|
||||
|
||||
# Determine output file and input files
|
||||
local -a input_files=()
|
||||
local output_file
|
||||
|
||||
if [[ -n "$PARSED_OUTPUT" ]]; then
|
||||
# Explicit output was specified via --output/-o
|
||||
output_file="$PARSED_OUTPUT"
|
||||
input_files=("${all_files[@]}")
|
||||
else
|
||||
# No explicit output, use default or check last argument
|
||||
output_file="collage.png"
|
||||
if [[ ${#all_files[@]} -ge 2 ]]; then
|
||||
local last_file="${all_files[-1]}"
|
||||
# If last file doesn't exist and has .png extension, treat as output
|
||||
if [[ ! -f "$last_file" && "$last_file" == *.png ]]; then
|
||||
output_file="$last_file"
|
||||
input_files=("${all_files[@]:0:$((${#all_files[@]} - 1))}")
|
||||
else
|
||||
input_files=("${all_files[@]}")
|
||||
fi
|
||||
else
|
||||
input_files=("${all_files[@]}")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Validate we have at least 2 input files
|
||||
if [[ ${#input_files[@]} -lt 2 ]]; then
|
||||
echo "Error: At least 2 input PNG files are required." >&2
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for required commands
|
||||
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 magick > /dev/null 2>&1; then
|
||||
echo "This script requires 'magick' (ImageMagick) but it wasn't found." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify all input files exist and are PNGs
|
||||
for img in "${input_files[@]}"; do
|
||||
if [[ ! -f "$img" ]]; then
|
||||
echo "Input file not found: $img" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! verify-png "$img"; then
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Create output directory if needed
|
||||
local outdir
|
||||
outdir="$(dirname "$output_file")"
|
||||
mkdir -p "$outdir"
|
||||
|
||||
# Create temporary working directory
|
||||
tmp_root="$(mktemp -d 2> /dev/null || mktemp -d -t pngs2collage)"
|
||||
trap 'cleanup "$tmp_root"' EXIT
|
||||
|
||||
# Calculate grid dimensions first
|
||||
local grid_dims
|
||||
grid_dims=$(calculate-grid-dimensions "${#input_files[@]}" \
|
||||
"$PARSED_MAX_COLUMNS")
|
||||
local cols rows
|
||||
read -r cols rows <<< "$grid_dims"
|
||||
|
||||
# Calculate cell size
|
||||
local cell_dims
|
||||
if ! cell_dims=$(calculate-cell-size "$PARSED_MAX_WIDTH" \
|
||||
"$PARSED_MAX_HEIGHT" "${input_files[@]}"); then
|
||||
exit 1
|
||||
fi
|
||||
local cell_size
|
||||
read -r cell_size _ <<< "$cell_dims"
|
||||
|
||||
# Prepare all images with position-specific padding
|
||||
local -a prepared_images=()
|
||||
local idx=0
|
||||
for img in "${input_files[@]}"; do
|
||||
# Calculate grid position (row, col) from index
|
||||
local row col
|
||||
row=$((idx / cols))
|
||||
col=$((idx % cols))
|
||||
|
||||
# Calculate padding for this position
|
||||
local padding
|
||||
padding=$(calculate-padding "$row" "$col" "$rows" "$cols" \
|
||||
"$PARSED_GAP" "$PARSED_BORDER")
|
||||
local top bottom left right
|
||||
read -r top bottom left right <<< "$padding"
|
||||
|
||||
# Prepare image with position-specific padding
|
||||
local prepared="${tmp_root}/prepared_${idx}.png"
|
||||
if ! prepare-image "$img" "$cell_size" "$top" "$bottom" "$left" \
|
||||
"$right" "$prepared"; then
|
||||
echo "Failed to prepare image: $img" >&2
|
||||
exit 1
|
||||
fi
|
||||
prepared_images+=("$prepared")
|
||||
idx=$((idx + 1))
|
||||
done
|
||||
|
||||
# Create the collage
|
||||
if ! create-collage "$cols" "$rows" "$PARSED_DPI" "$output_file" \
|
||||
"${prepared_images[@]}"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Done. Collage written to: $output_file"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
BIN
img/EmacsLG1-preview.png
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
img/EmacsLG2-preview.png
Normal file
|
After Width: | Height: | Size: 319 KiB |
BIN
img/EmacsLG3-preview.png
Normal file
|
After Width: | Height: | Size: 394 KiB |
BIN
img/logo.png
|
Before Width: | Height: | Size: 538 KiB After Width: | Height: | Size: 542 KiB |
|
Before Width: | Height: | Size: 336 KiB |
|
Before Width: | Height: | Size: 319 KiB |
|
Before Width: | Height: | Size: 389 KiB |