Created
December 1, 2025 13:01
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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