#!/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 simulator’s black edge pixels SHADOW_OPACITY=0 # 0–100 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/"