补充说明
该脚本使用 ffmpeg 的 -q:v 参数将常见格式图片批量转换为 WebP 格式,智能判断转换后体积是否更小,只在体积缩小时才替换原图,避免转换后文件反而变大。
一键脚本
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/ffmpeg_image_batch2webp.sh)
bash <(curl -sL ../sh/ffmpeg_image_batch2webp.sh)
# 跳过确认直接执行转换
bash <(curl -sL gitee.com/meimolihan/cmdbox/raw/master/sh/ffmpeg_image_batch2webp.sh) --yes
- 使用前先
cd到包含图片的目录再执行脚本
手动安装 ffmpeg
# Debian/Ubuntu
sudo apt-get update && sudo apt-get install -y ffmpeg
# CentOS/RHEL
sudo yum install -y epel-release && sudo yum install -y ffmpeg
# Arch Linux
sudo pacman -S --noconfirm ffmpeg
# Alpine
sudo apk add ffmpeg
功能特点
- 递归扫描当前目录及子目录下的所有图片文件
- 支持格式:jpg、jpeg、png、gif、bmp、tiff、tif(不区分大小写)
- 自动检测 ffmpeg,未安装时支持自动安装(apt/dnf/yum/zypper/pacman/apk)
- 预览模式:先列出待转换文件,确认后再执行
- 智能判断:转换后若 webp 文件体积 ≥ 原图,则跳过保留原图
- 转换时显示文件大小对比(KB)
- 完成后显示汇总统计:成功/跳过/失败
- 支持
--yes参数跳过所有确认,直接执行转换 - 可通过环境变量
QUALITY调整质量(默认80)
参数说明
| 参数 | 说明 | 默认值 |
|---|---|---|
--yes |
跳过所有确认提示,静默执行转换 | 未指定则需确认 |
QUALITY(环境变量) |
WebP 输出质量,值越小体积越小、画质越低 | 80 |
输出说明
脚本执行过程输出以下阶段:
| 阶段 | 说明 |
|---|---|
| ffmpeg 检测 | 自动检测并安装 ffmpeg(如需要) |
| 扫描文件 | 列出所有待转换的图片文件及目标路径 |
| 预览确认 | 显示待转换总数,等待用户确认(跳过确认模式跳过此步) |
| 转换进度 | 逐文件转换,显示 ✅ 成功 / ⏭️ 跳过 / ❌ 失败 |
| 汇总统计 | 显示成功转换、跳过、失败数量及总计 |
注意事项
- 需要 ffmpeg 已安装或有 sudo 权限自动安装
- 脚本在当前目录下递归扫描,执行前请先
cd到目标目录 - 转换后若 webp 体积 ≥ 原图体积,脚本会自动跳过并保留原图
- 转换成功后原图会被删除,仅保留
.webp文件 QUALITY值范围 0-100,值越小压缩率越高但画质越低- 脚本使用临时文件存储扫描结果,退出时自动清理
--yes模式会跳过所有交互确认,适合自动化任务
脚本源码
#!/bin/bash
set -euo pipefail
# 颜色定义
gl_hui='\033[38;5;59m'
gl_hong='\033[38;5;9m'
gl_lv='\033[38;5;10m'
gl_huang='\033[38;5;11m'
gl_lan='\033[38;5;32m'
gl_bai='\033[38;5;15m'
gl_zi='\033[38;5;13m'
gl_bufan='\033[38;5;14m'
# 日志函数
log_info() { echo -e "${gl_lan}[信息]${gl_bai} $*"; }
log_ok() { echo -e "${gl_lv}[成功]${gl_bai} $*"; }
log_warn() { echo -e "${gl_huang}[警告]${gl_bai} $*"; }
log_error() { echo -e "${gl_hong}[错误]${gl_bai} $*" >&2; }
# 确认函数
confirm() {
local prompt="${1:-是否继续?}"
local timeout=${2:-10}
echo -ne "${gl_huang}${prompt} (y/N) ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
read -r -t "$timeout" -n 1 ans || ans='n'
echo ""
case "$ans" in
y|Y) return 0 ;;
*) return 1 ;;
esac
}
# 安装 ffmpeg
install_ffmpeg() {
local pkg_manager=""
local install_cmd=""
local update_cmd=""
if command -v apt-get &>/dev/null; then
pkg_manager="apt-get"
install_cmd="apt-get install -y ffmpeg"
update_cmd="apt-get update"
elif command -v apt &>/dev/null; then
pkg_manager="apt"
install_cmd="apt install -y ffmpeg"
update_cmd="apt update"
elif command -v dnf &>/dev/null; then
pkg_manager="dnf"
install_cmd="dnf install -y ffmpeg"
elif command -v yum &>/dev/null; then
pkg_manager="yum"
install_cmd="yum install -y ffmpeg"
elif command -v zypper &>/dev/null; then
pkg_manager="zypper"
install_cmd="zypper install -y ffmpeg"
elif command -v pacman &>/dev/null; then
pkg_manager="pacman"
install_cmd="pacman -S --noconfirm ffmpeg"
elif command -v apk &>/dev/null; then
pkg_manager="apk"
install_cmd="apk add ffmpeg"
else
log_error "无法识别包管理器,请手动安装 ffmpeg"
return 1
fi
log_info "检测到包管理器:$pkg_manager,正在尝试安装 ffmpeg ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
local sudo_cmd=""
if [[ $EUID -ne 0 ]]; then
if command -v sudo &>/dev/null; then
sudo_cmd="sudo"
else
log_error "需要 root 权限但未找到 sudo,请以 root 用户运行或手动安装"
return 1
fi
fi
if [[ -n "$update_cmd" ]]; then
log_info "更新软件包索引 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
if ! $sudo_cmd $update_cmd; then
log_warn "更新索引失败,继续尝试安装 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
fi
fi
if ! $sudo_cmd $install_cmd; then
log_error "安装 ffmpeg 失败,请检查网络或手动安装"
return 1
fi
log_ok "ffmpeg 安装完成"
return 0
}
# 批量转换函数
convert_batch() {
local quality=${1:-80}
local parallel=${2:-4} # 并行数量
local temp_dir=$(mktemp -d)
trap 'rm -rf "$temp_dir"' RETURN
local file_list="$temp_dir/files.txt"
local success_list="$temp_dir/success.txt"
local skip_list="$temp_dir/skip.txt"
local fail_list="$temp_dir/fail.txt"
touch "$success_list" "$skip_list" "$fail_list"
# 收集所有图片文件
find . -type f \( \
-iname "*.jpg" -o \
-iname "*.jpeg" -o \
-iname "*.png" -o \
-iname "*.gif" -o \
-iname "*.bmp" -o \
-iname "*.tiff" -o \
-iname "*.tif" \) -print0 > "$file_list"
local total=$(grep -zc '.' "$file_list" 2>/dev/null || echo 0)
if [[ $total -eq 0 ]]; then
log_warn "没有找到匹配的图片文件"
return 0
fi
echo -e "\n${gl_zi}>>> 扫描完成,总计待处理文件:${total}${gl_bai}"
# 显示文件列表(最多显示10个)
echo -e "\n${gl_hui}文件列表(显示前10个):${gl_bai}"
head -n 10 "$file_list" | while IFS= read -r -d '' file; do
echo -e " ${gl_hui}•${gl_bai} $file"
done
if [[ $total -gt 10 ]]; then
echo -e " ${gl_hui}... 还有 $((total - 10)) 个文件${gl_bai}"
fi
# 确认
if [[ $DRY_RUN -eq 1 ]]; then
if ! confirm "是否开始批量转换 ${total} 个文件(质量 q=${quality})?" 30; then
log_info "用户取消,退出"
return 1
fi
fi
echo -e "\n${gl_zi}>>> 开始批量转换(并行数:${parallel})${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
echo -e "${gl_bufan}————————————————————————————————————————————————${gl_bai}"
local processed=0
# 使用 xargs 进行并行处理
convert_one() {
local src="$1"
local quality="$2"
local dst="${src%.*}.webp"
local tmpfile="${dst}.tmp.webp"
if [[ ! -f "$src" ]]; then
echo "FAIL|$src|文件不存在" >> "$fail_list"
return 1
fi
# 转换
ffmpeg -y -i "$src" -q:v "$quality" -compression_level 6 "$tmpfile" \
-hide_banner -loglevel error 2>/dev/null || true
if [[ -s "$tmpfile" ]]; then
local orig_size=$(stat -c%s "$src" 2>/dev/null || echo 0)
local new_size=$(stat -c%s "$tmpfile" 2>/dev/null || echo 0)
if (( new_size < orig_size )); then
mv "$tmpfile" "$dst"
rm -f "$src"
local orig_kb=$(awk -v s="$orig_size" 'BEGIN{printf "%.1f", s/1024}')
local new_kb=$(awk -v s="$new_size" 'BEGIN{printf "%.1f", s/1024}')
echo "SUCCESS|$src|$dst|${orig_kb}KB→${new_kb}KB" >> "$success_list"
return 0
else
rm -f "$tmpfile"
local orig_kb=$(awk -v s="$orig_size" 'BEGIN{printf "%.1f", s/1024}')
local new_kb=$(awk -v s="$new_size" 'BEGIN{printf "%.1f", s/1024}')
echo "SKIP|$src|${orig_kb}KB→${new_kb}KB (未缩小)" >> "$skip_list"
return 0
fi
else
rm -f "$tmpfile"
echo "FAIL|$src|转换失败" >> "$fail_list"
return 1
fi
}
export -f convert_one
export success_list skip_list fail_list
# 使用 xargs 并行处理
cat "$file_list" | xargs -0 -P "$parallel" -I {} bash -c 'convert_one "{}" "'"$quality"'"' || true
# 统计结果
local success_count=$(wc -l < "$success_list" 2>/dev/null || echo 0)
local skip_count=$(wc -l < "$skip_list" 2>/dev/null || echo 0)
local fail_count=$(wc -l < "$fail_list" 2>/dev/null || echo 0)
# 显示详细结果
echo -e "\n${gl_bufan}═══════════════════════════════════════${gl_bai}"
if [[ $success_count -gt 0 ]]; then
echo -e "${gl_lv}✅ 成功转换(${success_count} 个):${gl_bai}"
head -n 5 "$success_list" | while IFS='|' read -r _ src dst size; do
echo -e " ${gl_lv}•${gl_bai} $src → $dst ${gl_hui}($size)${gl_bai}"
done
if [[ $success_count -gt 5 ]]; then
echo -e " ${gl_hui}... 还有 $((success_count - 5)) 个${gl_bai}"
fi
fi
if [[ $skip_count -gt 0 ]]; then
echo -e "\n${gl_huang}⏭️ 跳过(${skip_count} 个,未缩小):${gl_bai}"
head -n 3 "$skip_list" | while IFS='|' read -r _ src size; do
echo -e " ${gl_huang}•${gl_bai} $src ${gl_hui}($size)${gl_bai}"
done
if [[ $skip_count -gt 3 ]]; then
echo -e " ${gl_hui}... 还有 $((skip_count - 3)) 个${gl_bai}"
fi
fi
if [[ $fail_count -gt 0 ]]; then
echo -e "\n${gl_hong}❌ 失败(${fail_count} 个):${gl_bai}"
head -n 3 "$fail_list" | while IFS='|' read -r _ src reason; do
echo -e " ${gl_hong}•${gl_bai} $src ${gl_hui}($reason)${gl_bai}"
done
if [[ $fail_count -gt 3 ]]; then
echo -e " ${gl_hui}... 还有 $((fail_count - 3)) 个${gl_bai}"
fi
fi
echo -e "\n${gl_bufan}═══════════════════════════════════════${gl_bai}"
echo -e "${gl_lv}🏁 全部任务结束${gl_bai}"
echo -e " ${gl_lv}✅ 成功转换:${gl_bai}${success_count}"
echo -e " ${gl_huang}⏭️ 跳过(未缩小):${gl_bai}${skip_count}"
echo -e " ${gl_hong}❌ 失败:${gl_bai}${fail_count}"
echo -e " ${gl_hui}总计处理:${gl_bai}$((success_count + skip_count + fail_count)) / ${total}"
echo -e "${gl_bufan}═══════════════════════════════════════${gl_bai}"
return 0
}
# 主程序开始
DRY_RUN=1
PARALLEL=4 # 默认并行数
QUALITY=80
# 解析参数
while [[ $# -gt 0 ]]; do
case "$1" in
--yes|-y)
DRY_RUN=0
shift
;;
--parallel|-p)
PARALLEL="$2"
shift 2
;;
--quality|-q)
QUALITY="$2"
shift 2
;;
--help|-h)
echo "用法: $0 [选项]"
echo "选项:"
echo " --yes, -y 跳过确认直接执行"
echo " --parallel N, -p N 并行处理数量(默认4)"
echo " --quality N, -q N 转换质量(默认80)"
echo " --help, -h 显示帮助信息"
exit 0
;;
*)
echo "未知参数: $1"
echo "使用 --help 查看帮助"
exit 1
;;
esac
done
# 检查 ffmpeg
if ! command -v ffmpeg &>/dev/null; then
log_warn "未找到 ffmpeg,尝试自动安装 ${gl_hong}.${gl_huang}.${gl_lv}.${gl_bai}"
if [[ $DRY_RUN -eq 1 ]]; then
if confirm "是否安装 ffmpeg(需要 sudo 权限)?" 30; then
if ! install_ffmpeg; then
log_error "安装失败,退出"
exit 1
fi
else
log_error "用户取消安装,无法继续"
exit 1
fi
else
if ! install_ffmpeg; then
log_error "安装失败,退出"
exit 1
fi
fi
if ! command -v ffmpeg &>/dev/null; then
log_error "ffmpeg 仍未安装成功,请手动处理"
exit 1
fi
log_ok "ffmpeg 已就绪"
fi
# 执行批量转换
clear
echo -e "${gl_zi}>>> 图片批量转换为 WebP 格式${gl_bai}"
echo -e "${gl_bufan}质量:${QUALITY},并行数:${PARALLEL}${gl_bai}"
convert_batch "$QUALITY" "$PARALLEL"
exit_code=$?
exit $exit_code
创建本地脚本
new_script="new_test.sh"
cat > "$new_script" << 'EOF'
#!/bin/bash
# 粘贴脚本源码
EOF
# 保留本地脚本,去掉 rm -f "$new_script"
chmod +x "$new_script" && ./"$new_script" && rm -f "$new_script"