Added the favicon generator
This commit is contained in:
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,3 +1,14 @@
|
||||
resources/
|
||||
public/
|
||||
.hugo_build.lock
|
||||
|
||||
# Favicon
|
||||
android-chrome-192x192.png
|
||||
android-chrome-512x512.png
|
||||
apple-touch-icon.png
|
||||
favicon-16x16.png
|
||||
favicon-32x32.png
|
||||
favicon-dark.svg
|
||||
favicon.ico
|
||||
favicon.svg
|
||||
site.webmanifest
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +1,6 @@
|
||||
[submodule "themes/hextra"]
|
||||
path = themes/hextra
|
||||
url = https://github.com/imfing/hextra.git
|
||||
[submodule "favicon/artwork"]
|
||||
path = favicon/artwork
|
||||
url = https://git.nicolabelluti.me/little-emulator/artwork
|
||||
|
||||
1
favicon/artwork
Submodule
1
favicon/artwork
Submodule
Submodule favicon/artwork added at 8ad897fd75
169
favicon/generate_favicons.sh
Executable file
169
favicon/generate_favicons.sh
Executable file
@@ -0,0 +1,169 @@
|
||||
#!/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"
|
||||
|
||||
# Dark variant color swap (case-insensitive)
|
||||
COLOR_A="#36373b"
|
||||
COLOR_B="#fafcfc"
|
||||
|
||||
# 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 "\033[1;31mError: Source SVG not found: ${SRC_SVG}\033[0m" >&2; exit 1; }
|
||||
command -v magick >/dev/null || { echo "\033[1;31mError: ImageMagick (magick) not found\033[0m" >&2; exit 1; }
|
||||
command -v xmlstarlet >/dev/null || { echo "\033[1;31mError: xmlstarlet required for SVG cropping\033[0m" >&2; exit 1; }
|
||||
|
||||
# ================================= Functions ==================================
|
||||
|
||||
##############################################
|
||||
# crop_svg_viewbox <input.svg> <output.svg> <target_size>
|
||||
# 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"
|
||||
}
|
||||
|
||||
##############################################
|
||||
# render_png <width> <height> <out_path> <background|'none'>
|
||||
##############################################
|
||||
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 by swapping ${COLOR_A} and ${COLOR_B}..."
|
||||
sed -E \
|
||||
-e "s/${COLOR_A//\#/\\#}/__TMP_COLOR__/Ig" \
|
||||
-e "s/${COLOR_B//\#/\\#}/${COLOR_A//\#/\\#}/Ig" \
|
||||
-e "s/__TMP_COLOR__/${COLOR_B//\#/\\#}/Ig" \
|
||||
"${OUTPUT_DIR}/favicon.svg" > "${OUTPUT_DIR}/favicon-dark.svg"
|
||||
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")"
|
||||
Reference in New Issue
Block a user