#!/bin/bash set -euo pipefail FONT_COLOR="#3C3C3C" # color for light text FONT_BOLD_COLOR="#B51700" # color for bold text ONLY_IPHONE=false ONLY_IPAD=false usage() { cat <<'EOF' Usage: frame_screens.sh [--iphone-only|--ipad-only] [SRC_ROOT] [BG_IMAGE] [OUT_ROOT] --iphone-only Only frame screenshots whose device slug is not iPad. --ipad-only Only frame screenshots whose device slug contains 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 ;; --ipad-only) ONLY_IPAD=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 [[ "$ONLY_IPHONE" == true && "$ONLY_IPAD" == true ]]; then echo "Cannot use --iphone-only and --ipad-only together." >&2 exit 1 fi 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-phone.png}" # default background image for iPhone shots 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 TITLE_POINTSIZE="${TITLE_POINTSIZE:-148}" # baseline point size for titles # 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 START_FRAME_IMAGE="${START_FRAME_IMAGE:-Shots/frame-start-iphone.png}" # static framed asset for the intro screen START_FRAME_FILENAME="${START_FRAME_FILENAME:-00-StartFrameTitle.png}" # output filename for the intro screen START_FRAME_TITLE_KEY="${START_FRAME_TITLE_KEY:-StartFrameTitle}" # key in the localization files START_FRAME_TITLE_Y="${START_FRAME_TITLE_Y:-$((TITLE_MARGIN - 100))}" # Y offset for the intro title text START_FRAME_POINTSIZE="${START_FRAME_POINTSIZE:-$TITLE_POINTSIZE}" # point size for intro title (defaults to standard) START_FRAME_RESIZE="${START_FRAME_RESIZE:-true}" # resize intro frame to target dimensions # 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:-Shots/frame-bg.png}" IPAD_CANVAS_MARGIN="${IPAD_CANVAS_MARGIN:-240}" IPAD_TITLE_MARGIN="${IPAD_TITLE_MARGIN:-180}" IPAD_FRAME_WIDTH="${IPAD_FRAME_WIDTH:-2200}" IPAD_FRAME_TOP_OFFSET="${IPAD_FRAME_TOP_OFFSET:-572}" IPAD_CORNER_RADIUS="${IPAD_CORNER_RADIUS:-60}" 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 point_size="${5:-$TITLE_POINTSIZE}" local line_spacing="${6:-$TITLE_LINE_SPACING}" 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 * 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 "$point_size" -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 "$point_size" -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 "$point_size" -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***" } should_render_start_frame() { local device_slug="$1" if [[ ! -f "$START_FRAME_IMAGE" ]]; then return 1 fi if [[ -z "$device_slug" ]]; then return 1 fi local slug_lower slug_lower="$(printf '%s' "$device_slug" | tr '[:upper:]' '[:lower:]')" if [[ "$slug_lower" == *"iphone"* ]]; then return 0 fi return 1 } render_start_frame_for_lang() { local output_dir="$1" local lang="$2" local log_prefix="$3" local target_width="$4" local target_height="$5" local canvas_margin="$6" local title_margin="$7" if [[ ! -f "$START_FRAME_IMAGE" ]]; then return 0 fi local title_text title_text="$(get_title "$lang" "$START_FRAME_TITLE_KEY")" if [[ -z "$title_text" || "$title_text" == "***NOT SET***" ]]; then echo "Skipped start frame (no title): ${log_prefix}/${START_FRAME_FILENAME}" return 0 fi mkdir -p "$output_dir" local dest="$output_dir/$START_FRAME_FILENAME" rm -f "$dest" local resize_flag resize_flag="$(printf '%s' "$START_FRAME_RESIZE" | tr '[:upper:]' '[:lower:]')" local working_canvas working_canvas="$(mktemp /tmp/start_frame_canvas.XXXXXX_$$.png)" if [[ "$resize_flag" == "true" && -n "${target_width:-}" && -n "${target_height:-}" ]]; then magick "$START_FRAME_IMAGE" -resize "${target_width}x${target_height}^" -gravity center -extent "${target_width}x${target_height}" "$working_canvas" else cp "$START_FRAME_IMAGE" "$working_canvas" fi local point_size="$START_FRAME_POINTSIZE" local scaled_title_y="$START_FRAME_TITLE_Y" local scaled_line_spacing="$TITLE_LINE_SPACING" if [[ -n "${target_height:-}" && -n "${canvas_margin:-}" && -n "${title_margin:-}" ]]; then local base_canvas_height=$(( target_height + 2 * canvas_margin + title_margin )) if (( base_canvas_height > 0 )); then read -r scaled_title_y scaled_line_spacing < /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_iphone_filter=true continue fi fi if [[ "$ONLY_IPAD" == 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_ipad_filter=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 ]]; then if [[ "$skipped_for_iphone_filter" == true ]]; then echo "No iPhone screenshots found under $SRC_ROOT" >&2 else echo "No screenshots found under $SRC_ROOT" >&2 fi elif [[ "$ONLY_IPAD" == true ]]; then if [[ "$skipped_for_ipad_filter" == true ]]; then echo "No iPad screenshots found under $SRC_ROOT" >&2 else echo "No screenshots found under $SRC_ROOT" >&2 fi else echo "No screenshots found under $SRC_ROOT" >&2 fi fi echo "Done. Framed images in: $OUT_ROOT/"