Skip to content

Instantly share code, notes, and snippets.

@evoactivity
Created March 3, 2026 01:53
Show Gist options
  • Select an option

  • Save evoactivity/2b8805877898c3683f4768cea2a93615 to your computer and use it in GitHub Desktop.

Select an option

Save evoactivity/2b8805877898c3683f4768cea2a93615 to your computer and use it in GitHub Desktop.
Animated WebP to MP4
#!/bin/bash
# ─── ANSI color codes ────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
SEP="${DIM}──────────────────────────────────────────────────${RESET}"
BOX_INNER=38
# ─── Defaults ─────────────────────────────────────────────────
original_action="move" # move | none | delete
move_dir=""
move_dir_set="no"
single_file="no"
output_name=""
# ─── Cleanup tracking ────────────────────────────────────────
current_tmpdir=""
current_output=""
interrupted="no"
# ─── Counters ─────────────────────────────────────────────────
total=0
processed=0
animated=0
converted=0
skipped=0
failed=0
# ─── Helper: summary row with dynamic padding ────────────────
print_summary_row() {
local label_color="$1"
local label="$2"
local value="$3"
local label_width=22
local value_len=${#value}
local trailing=$((BOX_INNER - 2 - label_width - value_len))
printf -v lbl "%-${label_width}s" "$label"
printf -v pad "%${trailing}s" ""
echo -e "${BOLD}${MAGENTA} ║${RESET} ${label_color}${lbl}${RESET}${BOLD}${value}${RESET}${pad}${BOLD}${MAGENTA}║${RESET}"
}
# ─── Print summary ────────────────────────────────────────────
print_summary() {
echo ""
echo -e "$SEP"
echo -e "${BOLD}${MAGENTA} ╔══════════════════════════════════════╗${RESET}"
if [ "$interrupted" = "yes" ]; then
echo -e "${BOLD}${MAGENTA} ║${RESET} ${RED}${BOLD}Interrupted -- Summary${RESET} ${BOLD}${MAGENTA}║${RESET}"
else
echo -e "${BOLD}${MAGENTA} ║ Summary ║${RESET}"
fi
echo -e "${BOLD}${MAGENTA} ╠══════════════════════════════════════╣${RESET}"
print_summary_row "$BLUE" "Total WebP files:" "$total"
print_summary_row "$DIM" "Processed:" "$processed"
print_summary_row "$YELLOW" "Animated:" "$animated"
print_summary_row "$GREEN" "Converted:" "$converted"
print_summary_row "$DIM" "Skipped (static):" "$skipped"
if [ "$failed" -gt 0 ]; then
print_summary_row "$RED" "Failed:" "$failed"
fi
if [ "$interrupted" = "yes" ]; then
local remaining=$((total - processed))
print_summary_row "$RED" "Not processed:" "$remaining"
fi
echo -e "${BOLD}${MAGENTA} ╚══════════════════════════════════════╝${RESET}"
echo ""
}
# ─── Signal handler ───────────────────────────────────────────
cleanup() {
interrupted="yes"
echo ""
echo -e " ${RED}${BOLD}!! Signal received -- cleaning up...${RESET}"
if [ -n "$current_tmpdir" ] && [ -d "$current_tmpdir" ]; then
rm -rf "$current_tmpdir"
echo -e " ${DIM} Removed temporary frames: ${current_tmpdir}${RESET}"
fi
if [ -n "$current_output" ] && [ -f "$current_output" ]; then
rm -f "$current_output"
echo -e " ${DIM} Removed partial output: $(basename "$current_output")${RESET}"
fi
print_summary
exit 130
}
trap cleanup SIGINT SIGTERM SIGHUP
# ─── Usage / Help ─────────────────────────────────────────────
show_help() {
echo ""
echo -e "${BOLD}${MAGENTA} Animated WebP -> MP4 Converter${RESET}"
echo ""
echo -e " ${BOLD}Usage:${RESET} $(basename "$0") [options] [input] [output]"
echo ""
echo -e " ${BOLD}Description:${RESET}"
echo -e " Converts animated WebP files to MP4 using ImageMagick and ffmpeg."
echo -e " Dimensions are adjusted to be divisible by 2 as required by h264."
echo ""
echo -e " ${BOLD}Positional arguments:${RESET}"
echo -e " ${CYAN}input${RESET} Directory to scan for .webp files, or a single .webp file"
echo -e " (default: current directory)"
echo -e " ${CYAN}output${RESET} Output directory for MP4 files (created if needed)"
echo -e " When input is a single file, this can be an .mp4 filename"
echo -e " (default: same location as input)"
echo ""
echo -e " ${BOLD}Options:${RESET}"
echo -e " ${CYAN}-h${RESET}, ${CYAN}--help${RESET} Show this help message and exit"
echo -e " ${CYAN}--move-to <dir>${RESET} Move originals to <dir> after conversion"
echo -e " (default: ${CYAN}original-webp/${RESET} beside input)"
echo -e " ${CYAN}--no-move${RESET} Leave original files in place"
echo -e " ${CYAN}--delete${RESET} Delete original files after successful conversion"
echo ""
echo -e " ${BOLD}Examples:${RESET}"
echo -e " $(basename "$0") Current dir, default settings"
echo -e " $(basename "$0") /path/to/webps Scan a directory"
echo -e " $(basename "$0") /path/to/webps ./output Dir input, MP4s to ./output"
echo -e " $(basename "$0") image.webp Convert one file"
echo -e " $(basename "$0") image.webp clip.mp4 Convert with custom output name"
echo -e " $(basename "$0") image.webp ./videos/ Single file to output directory"
echo -e " $(basename "$0") --delete ./input Delete originals after conversion"
echo -e " $(basename "$0") --no-move . Leave originals in place"
echo -e " $(basename "$0") --move-to /backup ./input ./output Custom move dir, in/out dirs"
echo ""
echo -e " ${BOLD}Required tools:${RESET}"
echo -e " ${DIM}webpmux${RESET} libwebp / webp package"
echo -e " ${DIM}convert${RESET} ImageMagick"
echo -e " ${DIM}identify${RESET} ImageMagick"
echo -e " ${DIM}ffmpeg${RESET} FFmpeg (with libx264)"
echo -e " ${DIM}awk${RESET} Usually pre-installed"
echo ""
exit 0
}
# ─── Parse arguments ──────────────────────────────────────────
positional=()
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
show_help
;;
--move-to)
if [ -z "$2" ] || [[ "$2" == -* ]]; then
echo -e "${RED}[error] --move-to requires a directory argument${RESET}"
exit 1
fi
original_action="move"
move_dir="$2"
move_dir_set="yes"
shift 2
;;
--no-move)
original_action="none"
shift
;;
--delete)
original_action="delete"
shift
;;
-*)
echo -e "${RED}[error] Unknown option: ${1}${RESET}"
echo -e " Run ${CYAN}$(basename "$0") --help${RESET} for usage info."
exit 1
;;
*)
positional+=("$1")
shift
;;
esac
done
if [ ${#positional[@]} -ge 3 ]; then
echo -e "${RED}[error] Too many positional arguments (max 2)${RESET}"
echo -e " Run ${CYAN}$(basename "$0") --help${RESET} for usage info."
exit 1
fi
input_path="${positional[0]:-.}"
output_path="${positional[1]:-}"
# ─── Determine mode: single file or directory ─────────────────
if [ -f "$input_path" ]; then
single_file="yes"
input_dir=$(cd "$(dirname "$input_path")" && pwd)
input_file=$(basename "$input_path")
if [ -n "$output_path" ]; then
case "$output_path" in
*.mp4)
output_name=$(basename "$output_path")
output_dir=$(dirname "$output_path")
;;
*)
output_name=""
output_dir="$output_path"
;;
esac
else
output_name=""
output_dir="$input_dir"
fi
elif [ -d "$input_path" ]; then
single_file="no"
input_dir=$(cd "$input_path" && pwd)
if [ -n "$output_path" ]; then
if [[ "$output_path" == *.mp4 ]]; then
echo -e "${RED}[error] Cannot use an .mp4 filename as output when input is a directory${RESET}"
echo -e " Use an output ${CYAN}directory${RESET} instead, or pass a single .webp file as input."
exit 1
fi
output_dir="$output_path"
else
output_dir="$input_dir"
fi
else
echo -e "${RED}[error] Input not found: ${input_path}${RESET}"
exit 1
fi
mkdir -p "$output_dir"
if [ "$original_action" = "move" ] && [ "$move_dir_set" = "no" ]; then
move_dir="$input_dir/original-webp"
fi
if [ "$original_action" = "move" ]; then
mkdir -p "$move_dir"
fi
# ─── Dependency check ─────────────────────────────────────────
required_tools=("webpmux" "convert" "identify" "ffmpeg" "awk")
missing=()
for tool in "${required_tools[@]}"; do
if ! command -v "$tool" > /dev/null 2>&1; then
missing+=("$tool")
fi
done
if [ ${#missing[@]} -gt 0 ]; then
echo ""
echo -e "${RED}${BOLD}[error] Missing required tool(s):${RESET}"
echo ""
for tool in "${missing[@]}"; do
case "$tool" in
webpmux) pkg="libwebp-tools / webp" ;;
convert|identify) pkg="ImageMagick" ;;
ffmpeg) pkg="FFmpeg (with libx264)" ;;
awk) pkg="awk (usually pre-installed)" ;;
*) pkg="unknown" ;;
esac
echo -e " ${RED}*${RESET} ${BOLD}${tool}${RESET} ${DIM}-- install via: ${pkg}${RESET}"
done
echo ""
echo -e " Install the missing tools and try again."
echo ""
exit 1
fi
echo -e "${GREEN}${DIM}[ok] All required tools found${RESET}"
# ─── Collect files ────────────────────────────────────────────
files_to_process=()
if [ "$single_file" = "yes" ]; then
files_to_process=("$input_dir/$input_file")
else
for f in "$input_dir"/*.webp; do
[ -e "$f" ] || continue
files_to_process+=("$f")
done
fi
total=${#files_to_process[@]}
if [ "$total" -eq 0 ]; then
echo -e "${YELLOW}[!] No .webp files found in: ${input_dir}${RESET}"
exit 0
fi
# ─── Display header ──────────────────────────────────────────
echo ""
echo -e "${BOLD}${MAGENTA} ╔══════════════════════════════════════╗${RESET}"
echo -e "${BOLD}${MAGENTA} ║ Animated WebP -> MP4 Converter ║${RESET}"
echo -e "${BOLD}${MAGENTA} ╚══════════════════════════════════════╝${RESET}"
echo ""
echo -e " ${DIM}Input: ${input_dir}${RESET}"
echo -e " ${DIM}Output: ${output_dir}${RESET}"
echo -e " ${DIM}Files: ${total} .webp file(s) found${RESET}"
case "$original_action" in
move) echo -e " ${DIM}Originals: move to ${move_dir}${RESET}" ;;
delete) echo -e " ${DIM}Originals: delete after conversion${RESET}" ;;
none) echo -e " ${DIM}Originals: leave in place${RESET}" ;;
esac
echo ""
# ─── Process files ────────────────────────────────────────────
for filepath in "${files_to_process[@]}"; do
file=$(basename "$filepath")
processed=$((processed + 1))
echo -e "$SEP"
echo -e "${BOLD}${BLUE}> [${processed}/${total}] Processing:${RESET} ${CYAN}${file}${RESET}"
echo -e "$SEP"
if webpmux -info "$filepath" 2>/dev/null | grep -qi "animation"; then
animated=$((animated + 1))
echo -e " ${YELLOW}[animated] Detected animated image${RESET}"
current_tmpdir=$(mktemp -d)
width=$(identify -format "%w" "${filepath}[0]")
height=$(identify -format "%h" "${filepath}[0]")
echo -e " ${DIM}[info] Original dimensions: ${width}x${height}${RESET}"
new_width=$((width - (width % 2)))
new_height=$((height - (height % 2)))
need_crop="no"
if [[ "$new_width" -ne "$width" ]]; then
need_crop="yes"
fi
if [[ "$new_height" -ne "$height" ]]; then
need_crop="yes"
fi
extract_ok="no"
if [ "$need_crop" = "yes" ]; then
echo -e " ${YELLOW}[crop] Adjusting to ${new_width}x${new_height} (h264 requires even dimensions)${RESET}"
if convert "$filepath" -coalesce -crop "${new_width}x${new_height}+0+0" +repage "$current_tmpdir/frame_%04d.png"; then
extract_ok="yes"
fi
else
if convert "$filepath" -coalesce "$current_tmpdir/frame_%04d.png"; then
extract_ok="yes"
fi
fi
if [ "$extract_ok" = "no" ]; then
echo -e " ${RED}[error] Failed to extract frames${RESET}"
failed=$((failed + 1))
rm -rf "$current_tmpdir"
current_tmpdir=""
echo ""
continue
fi
frames=("$current_tmpdir"/frame_*.png)
frame_count=${#frames[@]}
if [ "$frame_count" -eq 0 ]; then
echo -e " ${RED}[error] No frames produced${RESET}"
failed=$((failed + 1))
rm -rf "$current_tmpdir"
current_tmpdir=""
echo ""
continue
fi
echo -e " ${DIM}[info] Extracted ${frame_count} frames${RESET}"
delay=$(identify -format "%T" "${filepath}[0]")
if [ -z "$delay" ]; then
fps=10
elif [ "$delay" -eq 0 ] 2>/dev/null; then
fps=10
else
fps=$(awk "BEGIN {printf \"%.2f\", 100 / $delay}")
fi
echo -e " ${DIM}[info] Frame rate: ${fps} fps${RESET}"
if [ "$single_file" = "yes" ] && [ -n "$output_name" ]; then
current_output="$output_dir/$output_name"
else
current_output="$output_dir/${file%.*}.mp4"
fi
echo -e " ${DIM}[encode] Encoding with ffmpeg...${RESET}"
if ffmpeg -y -framerate "$fps" -i "$current_tmpdir/frame_%04d.png" -c:v libx264 -pix_fmt yuv420p -movflags +faststart "$current_output" 2>/dev/null; then
converted=$((converted + 1))
echo -e " ${GREEN}[done] Converted: $(basename "$current_output")${RESET}"
case "$original_action" in
move)
mv "$filepath" "$move_dir/"
echo -e " ${DIM}[move] Original moved to ${move_dir}/${RESET}"
;;
delete)
rm "$filepath"
echo -e " ${DIM}[delete] Original deleted${RESET}"
;;
none)
echo -e " ${DIM}[keep] Original left in place${RESET}"
;;
esac
else
echo -e " ${RED}[error] ffmpeg encoding failed${RESET}"
failed=$((failed + 1))
if [ -f "$current_output" ]; then
rm -f "$current_output"
fi
fi
rm -rf "$current_tmpdir"
current_tmpdir=""
current_output=""
else
skipped=$((skipped + 1))
echo -e " ${DIM}[skip] Not animated -- skipped${RESET}"
fi
echo ""
done
print_summary
@evoactivity
Copy link
Author

Animated WebP -> MP4 Converter

Usage:  webp-mp4 [options] [input] [output]

Description:
  Converts animated WebP files to MP4 using ImageMagick and ffmpeg.
  Dimensions are adjusted to be divisible by 2 as required by h264.

Positional arguments:
  input           Directory to scan for .webp files, or a single .webp file
                  (default: current directory)
  output          Output directory for MP4 files (created if needed)
                  When input is a single file, this can be an .mp4 filename
                  (default: same location as input)

Options:
  -h, --help            Show this help message and exit
  --move-to <dir>       Move originals to <dir> after conversion
                        (default: original-webp/ beside input)
  --no-move             Leave original files in place
  --delete              Delete original files after successful conversion

Examples:
  webp-mp4                                     Current dir, default settings
  webp-mp4 /path/to/webps                      Scan a directory
  webp-mp4 /path/to/webps ./output             Dir input, MP4s to ./output
  webp-mp4 image.webp                          Convert one file
  webp-mp4 image.webp clip.mp4                 Convert with custom output name
  webp-mp4 image.webp ./videos/                Single file to output directory
  webp-mp4 --delete ./input                    Delete originals after conversion
  webp-mp4 --no-move .                         Leave originals in place
  webp-mp4 --move-to /backup ./input ./output  Custom move dir, in/out dirs

Required tools:
  webpmux     libwebp / webp package
  convert     ImageMagick
  identify    ImageMagick
  ffmpeg      FFmpeg (with libx264)
  awk         Usually pre-installed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment