Skip to content

Instantly share code, notes, and snippets.

@dofy
Created December 1, 2025 13:01
Show Gist options
  • Select an option

  • Save dofy/5ad5e4ff2e6154487416816409295c75 to your computer and use it in GitHub Desktop.

Select an option

Save dofy/5ad5e4ff2e6154487416816409295c75 to your computer and use it in GitHub Desktop.
Recursively compress PNG images using pngquant, preserving the subdirectory structure of the input directory, and write the results to the output directory.
#!/usr/bin/env bash
# 默认参数
QUALITY=80
SPEED=5
ONLY_COMPRESSED=false
print_help() {
cat <<EOF
用法:
$(basename "$0") [选项] <输入目录> <输出目录> [文件名模式...]
说明:
使用 pngquant 递归压缩 PNG 图片,保持输入目录的子目录结构,
将结果写入输出目录。
- 默认只处理 PNG(pngquant 只支持 PNG),其它格式会被忽略。
- 若未提供文件名模式,则默认模式为 "*.png"。
- 默认会将“压缩后更大”的文件复制原图到输出目录。
- 若使用 --only-compressed,则只在输出目录保存“真正变小”的文件,
对于不变小的文件不会写入输出目录。
选项:
-q N, --quality N 设置压缩质量(如:80 或 65-85)
-s N, --speed N 设置压缩速度(1 最慢质量最好,5~7 更快)
--only-compressed 只在输出目录保存压缩后变小的文件
-h, --help 显示本帮助信息并退出
示例:
# 递归处理 ./images 下所有 PNG,输出到 ./out
$(basename "$0") ./images ./out
# 指定质量和速度,只处理匹配 *.png 和 icon_*.png 的文件
$(basename "$0") -q 75 -s 3 ./images ./out "*.png" "icon_*.png"
# 只保存真正压缩过的文件(不复制未变小的文件)
$(basename "$0") --only-compressed ./images ./out
EOF
}
# 解析可选参数:--quality / -q / --speed / -s / --only-compressed / --help
PARAMS=()
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
print_help
exit 0
;;
--only-compressed)
ONLY_COMPRESSED=true
shift
;;
--quality=*)
QUALITY="${1#*=}"
shift
;;
--quality)
QUALITY="$2"
shift 2
;;
-q)
QUALITY="$2"
shift 2
;;
--speed=*)
SPEED="${1#*=}"
shift
;;
--speed)
SPEED="$2"
shift 2
;;
-s)
SPEED="$2"
shift 2
;;
*)
PARAMS+=("$1")
shift
;;
esac
done
# 把去掉可选项后的参数还原
set -- "${PARAMS[@]}"
# 至少需要 2 个主要参数:输入目录、输出目录
if [ $# -lt 2 ]; then
echo "错误:缺少必要参数。" >&2
print_help >&2
exit 1
fi
INPUT_DIR="$1"
OUTPUT_DIR="$2"
shift 2
# 展开 ~(保持相对路径语义)
INPUT_DIR="${INPUT_DIR/#\~/$HOME}"
OUTPUT_DIR="${OUTPUT_DIR/#\~/$HOME}"
# 去掉 INPUT_DIR 末尾的斜杠,方便后面算相对路径
INPUT_DIR="${INPUT_DIR%/}"
# 文件名模式数组;若未指定,则默认 "*.png"
PATTERNS=("$@")
if [ ${#PATTERNS[@]} -eq 0 ]; then
PATTERNS=("*.png")
fi
# 检查 pngquant
if ! command -v pngquant >/dev/null 2>&1; then
echo "错误:未安装 pngquant,请先执行:brew install pngquant" >&2
exit 1
fi
# 输入目录存在性检查
if [ ! -d "$INPUT_DIR" ]; then
echo "错误:输入目录不存在:$INPUT_DIR" >&2
exit 1
fi
# 创建输出目录
mkdir -p "$OUTPUT_DIR"
# 获取文件大小(字节),兼容 macOS / Linux
get_size () {
if [ "$(uname)" = "Darwin" ]; then
stat -f%z "$1"
else
stat -c%s "$1"
fi
}
echo "== 使用参数 =="
echo "QUALITY = $QUALITY"
echo "SPEED = $SPEED"
echo "ONLY_COMPRESSED = $ONLY_COMPRESSED"
echo "输入目录 : $INPUT_DIR"
echo "输出目录 : $OUTPUT_DIR"
echo "文件名模式 : ${PATTERNS[*]}"
echo
compressed=0
kept_original=0
skipped=0
failed=0
for pattern in "${PATTERNS[@]}"; do
# 递归 find 匹配模式;注意:pngquant 只支持 PNG,这里假设模式就是 PNG 文件
while IFS= read -r file; do
[ -f "$file" ] || continue
rel_path="${file#$INPUT_DIR/}" # e.g. bg/layer1/foo.png
out_path="$OUTPUT_DIR/$rel_path"
out_dir="$(dirname "$out_path")"
mkdir -p "$out_dir"
orig_size_bytes=$(get_size "$file")
orig_size_kb=$(( (orig_size_bytes + 1023) / 1024 ))
echo "处理:$file"
echo " 原始大小:${orig_size_kb} KB"
pngquant \
--quality="$QUALITY" \
--speed="$SPEED" \
--strip \
--skip-if-larger \
--force \
--output "$out_path" \
"$file"
code=$?
if [ $code -eq 0 ]; then
# 压缩成功且变小
new_size_bytes=$(get_size "$out_path")
new_size_kb=$(( (new_size_bytes + 1023) / 1024 ))
ratio=$(awk "BEGIN {printf \"%.1f\", $new_size_bytes * 100 / $orig_size_bytes}")
echo " 压缩成功:${orig_size_kb} KB → ${new_size_kb} KB (${ratio}% )"
compressed=$((compressed + 1))
elif [ $code -eq 98 ]; then
# 压完反而更大
if [ "$ONLY_COMPRESSED" = true ]; then
# 只保留压缩成功的:不写入输出目录
rm -f "$out_path" 2>/dev/null
skipped=$((skipped + 1))
echo " 压缩后更大,跳过(未写入输出目录):${orig_size_kb} KB(100.0%)"
else
# 默认行为:输出目录保留原图
cp "$file" "$out_path"
kept_original=$((kept_original + 1))
echo " 压缩后更大,保留原图:${orig_size_kb} KB(100.0%)"
fi
else
failed=$((failed + 1))
echo " ⚠️ 压缩失败(退出码 $code),跳过该文件"
fi
echo
done < <(find "$INPUT_DIR" -type f -name "$pattern")
done
echo "== 完成 =="
echo "压缩成功(变小) :$compressed 个"
echo "保留原图(没变小,已输出):$kept_original 个"
echo "跳过(没变小,未输出) :$skipped 个"
echo "失败 :$failed 个"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment