Files
Cable/frame_screens.sh
2025-10-20 15:35:29 +02:00

408 lines
12 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
set -euo pipefail
FONT_COLOR="#3C3C3C" # color for light text
FONT_BOLD_COLOR="#B51700" # color for bold text
ONLY_IPHONE=false
usage() {
cat <<'EOF'
Usage: frame_screens.sh [--iphone-only] [SRC_ROOT] [BG_IMAGE] [OUT_ROOT]
--iphone-only Only frame screenshots whose device slug is not iPad.
SRC_ROOT Root folder with device/lang subfolders (default: Shots/Screenshots)
BG_IMAGE Background image to use (default: Shots/frame-bg.png)
OUT_ROOT Output folder for framed shots (default: Shots/Framed)
EOF
}
POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--iphone-only)
ONLY_IPHONE=true
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
POSITIONAL_ARGS+=("$@")
break
;;
-*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
*)
POSITIONAL_ARGS+=("$1")
shift
;;
esac
done
if ((${#POSITIONAL_ARGS[@]})); then
set -- "${POSITIONAL_ARGS[@]}"
else
set --
fi
# Inputs
SRC_ROOT="${1:-Shots/Screenshots}" # root folder with device/lang subfolders (iphone-17-pro-max/en/, ipad-pro-13-inch-m4/de/…)
BG_IMAGE="${2:-Shots/frame-bg.png}" # default background image
OUT_ROOT="${3:-Shots/Framed}" # output folder
FONT="./Shots/Fonts/Oswald-Light.ttf" # font for title text
FONT_BOLD="./Shots/Fonts/Oswald-SemiBold.ttf" # font for *emphasized* text
# Tweakables
CORNER_RADIUS="auto" # corner radius; "auto" picks a good value based on width
INSET=2 # inset (px) to shave off simulators black edge pixels
SHADOW_OPACITY=0 # 0100
SHADOW_BLUR=20 # blur radius
SHADOW_OFFSET_X=0 # px
SHADOW_OFFSET_Y=40 # px
CANVAS_MARGIN=245 # default margin around the device on the background, px
TITLE_MARGIN=378 # default margin above the device for title text, px
TITLE_LINE_SPACING=220 # vertical distance between lines of the title text, px
# Device-specific overrides (can be tuned via env vars)
TARGET_WIDTH_PHONE="${TARGET_WIDTH_PHONE:-1320}"
TARGET_HEIGHT_PHONE="${TARGET_HEIGHT_PHONE:-2868}"
TARGET_WIDTH_IPAD="${TARGET_WIDTH_IPAD:-2048}"
TARGET_HEIGHT_IPAD="${TARGET_HEIGHT_IPAD:-2732}"
IPAD_BG_IMAGE="${IPAD_BG_IMAGE:-$BG_IMAGE}"
IPAD_CANVAS_MARGIN="${IPAD_CANVAS_MARGIN:-240}"
IPAD_TITLE_MARGIN="${IPAD_TITLE_MARGIN:-180}"
mkdir -p "$OUT_ROOT"
# Function to render mixed-font text (light + semi-bold for *text*)
render_mixed_font_title() {
local canvas="$1"
local title_text="$2"
local title_y="$3"
local output="$4"
local expanded_title
expanded_title="$(printf '%b' "$title_text")"
local temp_img
temp_img="$(mktemp /tmp/text_temp.XXXXXX_$$.png)"
cp "$canvas" "$temp_img"
read -r canvas_w canvas_h <<<"$(identify -format "%w %h" "$canvas")"
local -a lines=()
while IFS= read -r line || [[ -n "$line" ]]; do
lines+=("$line")
done < <(printf '%s' "$expanded_title")
if ((${#lines[@]} == 0)); then
lines+=("$expanded_title")
fi
if ((${#lines[@]} > 2)); then
lines=("${lines[@]:0:2}")
fi
for idx in "${!lines[@]}"; do
local line="${lines[$idx]}"
local current_y=$((title_y + idx * TITLE_LINE_SPACING))
local -a text_segments=()
local -a font_types=()
local current_text=""
local in_bold=false
local i=0
local line_length=${#line}
while [ $i -lt $line_length ]; do
local char="${line:$i:1}"
if [[ "$char" == "*" ]]; then
text_segments+=("$current_text")
if [[ "$in_bold" == true ]]; then
font_types+=("bold")
else
font_types+=("light")
fi
current_text=""
if [[ "$in_bold" == true ]]; then
in_bold=false
else
in_bold=true
fi
else
current_text+="$char"
fi
i=$((i + 1))
done
text_segments+=("$current_text")
if [[ "$in_bold" == true ]]; then
font_types+=("bold")
else
font_types+=("light")
fi
local total_width=0
for ((j = 0; j < ${#text_segments[@]}; j++)); do
local segment="${text_segments[$j]}"
if [[ -n "$segment" ]]; then
local font_for_measurement="$FONT"
if [[ "${font_types[$j]}" == "bold" ]]; then
font_for_measurement="$FONT_BOLD"
fi
local segment_for_measurement="$segment"
segment_for_measurement="${segment_for_measurement/#/ }"
segment_for_measurement="${segment_for_measurement/%/ }"
local part_width
part_width=$(magick -font "$font_for_measurement" -pointsize 148 -size x label:"$segment_for_measurement" -format "%w" info:)
total_width=$((total_width + part_width))
fi
done
if (( total_width <= 0 )); then
continue
fi
local start_x=$(( (canvas_w - total_width) / 2 ))
local x_offset=0
for ((j = 0; j < ${#text_segments[@]}; j++)); do
local segment="${text_segments[$j]}"
if [[ -n "$segment" ]]; then
local font_to_use="$FONT"
local color_to_use="$FONT_COLOR"
if [[ "${font_types[$j]}" == "bold" ]]; then
font_to_use="$FONT_BOLD"
color_to_use="$FONT_BOLD_COLOR"
fi
local segment_for_rendering="$segment"
segment_for_rendering="${segment_for_rendering/#/ }"
segment_for_rendering="${segment_for_rendering/%/ }"
magick "$temp_img" \
-font "$font_to_use" -pointsize 148 -fill "$color_to_use" \
-gravity northwest -annotate "+$((start_x + x_offset))+${current_y}" "$segment_for_rendering" \
"$temp_img"
local text_width
text_width=$(magick -font "$font_to_use" -pointsize 148 -size x label:"$segment_for_rendering" -format "%w" info:)
x_offset=$((x_offset + text_width))
fi
done
done
cp "$temp_img" "$output"
rm -f "$temp_img"
}
# Function to get title from config file
get_title() {
local lang="$1"
local screenshot_name="$2"
local config_file="./Shots/Titles/${lang}.conf"
# Extract view name from filename format: 03-LoadEditorView_0_5EE662BD-84C7-41AC-806E-EB8C7340A037.png
# Remove .png extension, then extract the part after the first dash and before the first underscore
local base_name=$(basename "$screenshot_name" .png)
# Remove leading number and dash (e.g., "03-")
base_name=${base_name#*-}
# Remove everything from the first underscore onwards (e.g., "_0_5EE662BD...")
base_name=${base_name%%_*}
# Try to find title in config file
if [[ -f "$config_file" ]]; then
local title=$(grep "^${base_name}=" "$config_file" 2>/dev/null | cut -d'=' -f2-)
if [[ -n "$title" ]]; then
echo "$title"
return
fi
fi
# Fallback to default title
echo "***NOT SET***"
}
# Function to frame one screenshot
frame_one () {
local in="$1" # input screenshot (e.g., 1320x2868)
local out="$2" # output image
local bg="$3"
local lang="$4" # language code (e.g., "de", "en")
local screenshot_name="$5" # screenshot filename
local target_width="$6"
local target_height="$7"
local canvas_margin="$8"
local title_margin="$9"
# Read sizes
read -r W H <<<"$(identify -format "%w %h" "$in")"
# Determine corner radius
local R
if [[ "$CORNER_RADIUS" == "auto" ]]; then
# Heuristic: ~1/12 of width works well for iPhone 6.9" (≈110px for 1320px width)
R=$(( W / 12 ))
else
R=$CORNER_RADIUS
fi
# Create rounded-corner mask the same size as the screenshot
local mask
mask="$(mktemp /tmp/mask.XXXXXX_$$.png)"
magick -size "${W}x${H}" xc:black \
-fill white -draw "roundrectangle ${INSET},${INSET},$((W-1-INSET)),$((H-1-INSET)),$R,$R" \
"$mask"
# Apply rounded corners + make a soft drop shadow
# 1) Rounded PNG
local rounded
rounded="$(mktemp /tmp/rounded.XXXXXX_$$.png)"
magick "$in" -alpha set "$mask" -compose copyopacity -composite "$rounded"
# 2) Shadow from rounded image
local shadow
shadow="$(mktemp /tmp/shadow.XXXXXX_$$.png)"
magick "$rounded" \
\( +clone -background black -shadow ${SHADOW_OPACITY}x${SHADOW_BLUR}+${SHADOW_OFFSET_X}+${SHADOW_OFFSET_Y} \) \
+swap -background none -layers merge +repage "$shadow"
# Compose on the background, centered
# First, scale background to be at least screenshot+margin in both dimensions
read -r BW BH <<<"$(identify -format "%w %h" "$bg")"
local minW=$((W + 2*canvas_margin))
local minH=$((H + 2*canvas_margin + title_margin))
local canvas
canvas="$(mktemp /tmp/canvas.XXXXXX_$$.png)"
magick "$bg" -resize "${minW}x${minH}^" -gravity center -extent "${minW}x${minH}" "$canvas"
# Add title text above the screenshot
local title_text=$(get_title "$lang" "$screenshot_name")
local with_title
with_title="$(mktemp /tmp/with_title.XXXXXX_$$.png)"
# Calculate title position (center horizontally, positioned above the screenshot)
local title_y=$((title_margin - 100)) # 10px from top of title margin
# Render title with mixed fonts
render_mixed_font_title "$canvas" "$title_text" "$title_y" "$with_title"
# Now place shadow (which already includes the rounded image) positioned below the title
# Calculate the vertical offset to center the screenshot in the remaining space below the title
local screenshot_offset=$((title_margin*2))
local temp_result
temp_result="$(mktemp /tmp/temp_result.XXXXXX_$$.png)"
magick "$with_title" "$shadow" -gravity center -geometry "+0+${screenshot_offset}" -compose over -composite "$temp_result"
# Final step: scale to exact dimensions 1320 × 2868px
magick "$temp_result" -resize "${target_width}x${target_height}^" -gravity center -extent "${target_width}x${target_height}" "$out"
rm -f "$mask" "$rounded" "$shadow" "$canvas" "$with_title" "$temp_result"
}
# Process all screenshots in SRC_ROOT/*/*.png
resolve_device_profile() {
local device_slug="$1"
PROFILE_BG="$BG_IMAGE"
PROFILE_TARGET_WIDTH="$TARGET_WIDTH_PHONE"
PROFILE_TARGET_HEIGHT="$TARGET_HEIGHT_PHONE"
PROFILE_CANVAS_MARGIN="$CANVAS_MARGIN"
PROFILE_TITLE_MARGIN="$TITLE_MARGIN"
if [[ -n "$device_slug" ]]; then
local slug_lower
slug_lower="$(printf '%s' "$device_slug" | tr '[:upper:]' '[:lower:]')"
if [[ "$slug_lower" == *"ipad"* ]]; then
PROFILE_BG="$IPAD_BG_IMAGE"
PROFILE_TARGET_WIDTH="$TARGET_WIDTH_IPAD"
PROFILE_TARGET_HEIGHT="$TARGET_HEIGHT_IPAD"
PROFILE_CANVAS_MARGIN="$IPAD_CANVAS_MARGIN"
PROFILE_TITLE_MARGIN="$IPAD_TITLE_MARGIN"
fi
fi
}
process_lang_dir() {
local lang_path="$1"
local lang="$2"
local device_slug="$3"
local out_dir="$OUT_ROOT"
local log_prefix="$lang"
if [[ -n "$device_slug" ]]; then
out_dir="$out_dir/$device_slug"
log_prefix="$device_slug/$lang"
fi
out_dir="$out_dir/$lang"
mkdir -p "$out_dir"
resolve_device_profile "$device_slug"
shopt -s nullglob
for shot in "$lang_path"/*.png; do
local base="$(basename "$shot")"
frame_one \
"$shot" \
"$out_dir/$base" \
"$PROFILE_BG" \
"$lang" \
"$base" \
"$PROFILE_TARGET_WIDTH" \
"$PROFILE_TARGET_HEIGHT" \
"$PROFILE_CANVAS_MARGIN" \
"$PROFILE_TITLE_MARGIN"
echo "Framed: $log_prefix/$base"
done
}
shopt -s nullglob
found_any=false
skipped_for_device=false
for entry in "$SRC_ROOT"/*; do
[[ -d "$entry" ]] || continue
entry_basename="$(basename "$entry")"
entry_lower="$(printf '%s' "$entry_basename" | tr '[:upper:]' '[:lower:]')"
if [[ "$ONLY_IPHONE" == true && "$entry_lower" == *"ipad"* ]]; then
skipped_for_device=true
continue
fi
pattern="${entry%/}/*.png"
if compgen -G "$pattern" > /dev/null; then
process_lang_dir "$entry" "$(basename "$entry")" ""
found_any=true
continue
fi
for langdir in "$entry"/*; do
[[ -d "$langdir" ]] || continue
if [[ "$ONLY_IPHONE" == true ]]; then
lang_device_slug="$(basename "$entry")"
lang_slug_lower="$(printf '%s' "$lang_device_slug" | tr '[:upper:]' '[:lower:]')"
if [[ "$lang_slug_lower" == *"ipad"* ]]; then
skipped_for_device=true
continue
fi
fi
pattern="${langdir%/}/*.png"
if compgen -G "$pattern" > /dev/null; then
process_lang_dir "$langdir" "$(basename "$langdir")" "$(basename "$entry")"
found_any=true
fi
done
done
if [[ "$found_any" == false ]]; then
if [[ "$ONLY_IPHONE" == true && "$skipped_for_device" == true ]]; then
echo "No iPhone screenshots found under $SRC_ROOT" >&2
else
echo "No screenshots found under $SRC_ROOT" >&2
fi
fi
echo "Done. Framed images in: $OUT_ROOT/"