#!/usr/bin/env bash # Make the script stop if something goes wrong: # -e: any command fails # -u: unset variable is used # -o pipefail: a pipeline fails if any command fails set -euo pipefail # Show a clear error before exiting trap 'echo -e "\033[1;31mError at line $LINENO\033[0m" >&2; exit 1' ERR # =================================== CONFIG =================================== # Paths SRC_SVG="$(dirname "$BASH_SOURCE[0]")/artwork/logo.svg" OUTPUT_DIR="$(dirname "$BASH_SOURCE[0]")/../static" # Cropping CROP_SIZE=800 # final square viewBox size (e.g., 800x800) # Branding / Manifest (for site.webmanifest) APP_NAME="Little Emulator" THEME_COLOR="#000000" BACKGROUND_COLOR="#000000" # Color swap map for dark SVG generation COLOR_SWAP_MAP=( "36373b fafcfc" "fafcfc 000000" ) # Raster outputs # size file_name background PNG_LIST=( "192 android-chrome-192x192.png none" "512 android-chrome-512x512.png none" "180 apple-touch-icon.png white" "16 favicon-16x16.png none" "32 favicon-32x32.png none" ) # ICO sizes ICO_SIZES=(16 32 48) # ============================= Pre-flight checks ============================== [[ -f "${SRC_SVG}" ]] || { echo -e "\033[1;31mError: Source SVG not found: ${SRC_SVG}\033[0m" >&2; exit 1; } command -v magick >/dev/null || { echo -e "\033[1;31mError: ImageMagick (magick) not found\033[0m" >&2; exit 1; } command -v xmlstarlet >/dev/null || { echo -e "\033[1;31mError: xmlstarlet required for SVG cropping\033[0m" >&2; exit 1; } # ================================= Functions ================================== ############################################## # crop_svg_viewbox # Rewrites the SVG viewBox to a centered square ############################################## crop_svg_viewbox() { local INPUT_FILE="$1" local OUTPUT_FILE="$2" local TARGET_SIZE="$3" local VIEWBOX MIN_X MIN_Y VIEWBOX_WIDTH VIEWBOX_HEIGHT local NEW_MIN_X NEW_MIN_Y NEW_SIZE # Normalize: if no viewBox, derive it from width/height (viewBox="0 0 w h") VIEWBOX="$(xmlstarlet sel -t -v "string(/*[local-name()='svg']/@viewBox)" "$INPUT_FILE" || :)" if [[ $VIEWBOX =~ [^[:space:]] ]]; then read -r MIN_X MIN_Y VIEWBOX_WIDTH VIEWBOX_HEIGHT <<<"$(tr ',' ' ' <<<"$VIEWBOX")" else VIEWBOX_WIDTH="$(xmlstarlet sel -t -v "string(/*[local-name()='svg']/@width)" "$INPUT_FILE")" VIEWBOX_HEIGHT="$(xmlstarlet sel -t -v "string(/*[local-name()='svg']/@height)" "$INPUT_FILE")" VIEWBOX_WIDTH="${VIEWBOX_WIDTH//[!0-9.+-]/}" VIEWBOX_HEIGHT="${VIEWBOX_HEIGHT//[!0-9.+-]/}" MIN_X=0 MIN_Y=0 VIEWBOX="$MIN_X $MIN_Y $VIEWBOX_WIDTH $VIEWBOX_HEIGHT" fi # Compute centered square read -r NEW_MIN_X NEW_MIN_Y NEW_SIZE < <(awk " BEGIN { SIDE = ($VIEWBOX_WIDTH < $TARGET_SIZE ? $VIEWBOX_WIDTH : $TARGET_SIZE) if ($VIEWBOX_HEIGHT < SIDE) SIDE = $VIEWBOX_HEIGHT CENTER_X = $MIN_X + ($VIEWBOX_WIDTH - SIDE) / 2.0 CENTER_Y = $MIN_Y + ($VIEWBOX_HEIGHT - SIDE) / 2.0 print CENTER_X, CENTER_Y, SIDE } ") # Apply normalized centered square viewBox, remove width/height, enforce centered rendering xmlstarlet ed \ --update "/*[local-name()='svg']/@viewBox" -v "$NEW_MIN_X $NEW_MIN_Y $NEW_SIZE $NEW_SIZE" \ --insert "/*[local-name()='svg'][not(@viewBox)]" \ --type attr --name "viewBox" --value "$NEW_MIN_X $NEW_MIN_Y $NEW_SIZE $NEW_SIZE" \ \ --delete "/*[local-name()='svg']/@width" \ --delete "/*[local-name()='svg']/@height" \ --update "/*[local-name()='svg']/@preserveAspectRatio" -v "xMidYMid meet" \ "$INPUT_FILE" > "$OUTPUT_FILE" } ############################################## # swap_svg_colors <"SRC DEST"...> # Performs case-insensitive hex color swaps using two-pass placeholders ############################################## swap_svg_colors() { local INPUT_FILE="$1" local OUTPUT_FILE="$2" shift 2 local MAP_ITEMS=("$@") if (( ${#MAP_ITEMS[@]} == 0 )); then cp -- "$INPUT_FILE" "$OUTPUT_FILE" return fi local SED_ARGS_PRE=() local SED_ARGS_POST=() local I=0 SRC DEST PLACEHOLDER for pair in "${MAP_ITEMS[@]}"; do read -r SRC DEST <<<"$pair" SRC="${SRC#\#}" DEST="${DEST#\#}" PLACEHOLDER="__TMP_COLOR_${I}__" SED_ARGS_PRE+=(-e "s/#?${SRC}\\b/${PLACEHOLDER}/Ig") SED_ARGS_POST+=(-e "s/${PLACEHOLDER}/#${DEST}/g") I=$((I+1)) done sed -E "${SED_ARGS_PRE[@]}" -- "$INPUT_FILE" | sed -E "${SED_ARGS_POST[@]}" -- > "$OUTPUT_FILE" } ############################################## # render_png ############################################## render_png() { local SIZE="$1" INPUT="$2" OUTPUT="$3" BACKGROUND_COLOR="${4:-none}" magick -density 384 "${INPUT}" \ -background "${BACKGROUND_COLOR}" -alpha remove -alpha off \ -resize "${SIZE}x${SIZE}" -gravity center -extent "${SIZE}x${SIZE}" \ -strip "${OUTPUT}" } # ==================================== Main ==================================== mkdir -p "${OUTPUT_DIR}" echo "▶️ Cropping SVG to centered ${CROP_SIZE}x${CROP_SIZE} viewBox..." crop_svg_viewbox "${SRC_SVG}" "${OUTPUT_DIR}/favicon.svg" "${CROP_SIZE}" echo "✅ Cropped vector saved: favicon.svg" echo "▶️ Creating dark SVG variant..." swap_svg_colors "${OUTPUT_DIR}/favicon.svg" "${OUTPUT_DIR}/favicon-dark.svg" "${COLOR_SWAP_MAP[@]}" echo "✅ Dark variant saved: favicon-dark.svg" echo "▶️ Rendering PNG assets..." for ITEM in "${PNG_LIST[@]}"; do read -r SIZE NAME BG <<<"$ITEM" render_png "$SIZE" "${OUTPUT_DIR}/favicon.svg" "${OUTPUT_DIR}/${NAME}" "${BG}" echo " • ${NAME} (${SIZE}px)" done echo "✅ PNGs done." echo "▶️ Building multi-size ICO..." TMP_PNG_DIR="$(mktemp -d)" trap 'rm -rf "${TMP_PNG_DIR}"' EXIT ICO_INPUTS=() for SIZE in "${ICO_SIZES[@]}"; do render_png "$SIZE" "${OUTPUT_DIR}/favicon.svg" "${TMP_PNG_DIR}/${SIZE}.png" "none" ICO_INPUTS+=("${TMP_PNG_DIR}/${SIZE}.png") done magick "${ICO_INPUTS[@]}" -colors 256 "${OUTPUT_DIR}/favicon.ico" echo "✅ ICO saved: favicon.ico" echo "▶️ Writing site.webmanifest..." ICON_ITEMS=() for ITEM in "${PNG_LIST[@]}"; do read -r SIZE NAME BG <<<"$ITEM" [[ "$NAME" == android-* ]] || continue ITEM='{ "src": "/'"$NAME"'", "sizes": "'"$SIZE"'x'"$SIZE"'", "type": "image/png" }' ICON_ITEMS+=("$ITEM") done ICON_JSON="$(printf '%s\n' "${ICON_ITEMS[@]}" | paste -sd, -)" cat > "${OUTPUT_DIR}/site.webmanifest" << JSON { "name": "${APP_NAME}", "short_name": "${APP_NAME}", "icons": [ ${ICON_JSON} ], "theme_color": "${THEME_COLOR}", "background_color": "${BACKGROUND_COLOR}", "display": "standalone" } JSON echo "✅ Manifest saved: site.webmanifest" echo "🎉 All assets written to: $(readlink -f "$OUTPUT_DIR")"