Skip to content

Instantly share code, notes, and snippets.

@RexYuan
Created March 20, 2026 23:38
Show Gist options
  • Select an option

  • Save RexYuan/b502b8328100f18f98150d586ba2f52b to your computer and use it in GitHub Desktop.

Select an option

Save RexYuan/b502b8328100f18f98150d586ba2f52b to your computer and use it in GitHub Desktop.
TinyPNG-like PNG compression pipeline using pngquant(lossy) followed by oxipng(lossless optimization).
#!/usr/bin/env bash
#
# tinypng.sh
#
# TinyPNG-like PNG compression pipeline using pngquant (lossy)
# followed by oxipng (lossless optimization).
#
# Notes: This script was vibe-coded but checked in-head by me.
#
# Strategy:
# - Validate input files are PNG
# - Apply perceptual palette quantization via pngquant
# - Enforce quality bounds (fail if too lossy)
# - Optimize resulting PNG with oxipng (DEFLATE tuning + stripping)
# - Discard output if larger than original (unless overridden)
# - Report size reduction with human-readable units and % saved
#
# Guarantees:
# - No overwrite of original files
# - No silently worse outputs
# - Explicit failure reporting at every stage
#
# Usage:
# ./tinypng.sh [options] file1.png [file2.png ...]
#
# Options:
# -q, --quality MIN-MAX Quality range for pngquant (default: 60-80)
# -s, --speed N pngquant speed 1–11 (default: 1)
# -o, --optimization N oxipng level 0–6 (default: 4)
# -j, --jobs N parallel jobs (default: 1)
# --no-keep keep output even if larger
# -h, --help show help
#
# Output:
# input.png → input_tiny.png
#
# Examples:
# ./tinypng.sh image.png
# ./tinypng.sh -q 70-90 *.png
# ./tinypng.sh -j 8 *.png
#
# Requirements:
# - pngquant
# - oxipng
#
# Install:
# brew install pngquant oxipng
set -uo pipefail
########################################
# Defaults (TinyPNG-like)
########################################
QUALITY="60-80"
SPEED=1
OXI_LEVEL=4
KEEP_IF_LARGER=1
JOBS=1
########################################
# Logging helpers
########################################
log() { echo "[INFO] $*"; }
warn() { echo "[WARN] $*" >&2; }
fail() { echo "[ERROR] $*" >&2; }
########################################
# Help
########################################
usage() {
cat <<EOF
Usage: tinypng.sh [options] file1.png [file2.png ...]
Options:
-q, --quality MIN-MAX (default: 60-80)
-s, --speed N 1-11 (default: 1)
-o, --optimization N 0-6 (default: 4)
-j, --jobs N parallel jobs (default: 1)
--no-keep keep output even if larger
-h, --help show help
Output:
input.png → input_tiny.png
EOF
}
########################################
# Validation
########################################
validate_quality() {
[[ "$1" =~ ^[0-9]{1,3}-[0-9]{1,3}$ ]] || { fail "Invalid quality format: $1"; return 1; }
local min="${1%-*}"
local max="${1#*-}"
(( min >= 0 && max <= 100 && min <= max )) || { fail "Invalid quality range: $1"; return 1; }
}
validate_speed() {
[[ "$1" =~ ^[0-9]+$ ]] || { fail "Invalid speed: $1"; return 1; }
(( $1 >= 1 && $1 <= 11 )) || { fail "Speed must be 1-11"; return 1; }
}
validate_oxi() {
[[ "$1" =~ ^[0-9]+$ ]] || { fail "Invalid optimization level: $1"; return 1; }
(( $1 >= 0 && $1 <= 6 )) || { fail "Optimization must be 0-6"; return 1; }
}
########################################
# Parse args
########################################
FILES=()
while [[ $# -gt 0 ]]; do
case "$1" in
-q|--quality) QUALITY="$2"; validate_quality "$QUALITY" || exit 1; shift 2;;
-s|--speed) SPEED="$2"; validate_speed "$SPEED" || exit 1; shift 2;;
-o|--optimization) OXI_LEVEL="$2"; validate_oxi "$OXI_LEVEL" || exit 1; shift 2;;
-j|--jobs) JOBS="$2"; shift 2;;
--no-keep) KEEP_IF_LARGER=0; shift;;
-h|--help) usage; exit 0;;
*) FILES+=("$1"); shift;;
esac
done
[[ ${#FILES[@]} -eq 0 ]] && { usage; exit 1; }
########################################
# Dependency check
########################################
command -v pngquant >/dev/null || { fail "pngquant not found"; exit 1; }
command -v oxipng >/dev/null || { fail "oxipng not found"; exit 1; }
########################################
# Process function
########################################
process_file() {
local input="$1"
log "Processing: $input"
# Validate file
[[ -f "$input" ]] || { fail "$input does not exist"; return 1; }
[[ "$input" == *.png ]] || { fail "$input is not a .png file"; return 1; }
local dir base tmp out
dir="$(dirname "$input")"
base="$(basename "$input" .png)"
tmp="$dir/${base}.tmp.png"
out="$dir/${base}_tiny.png"
########################################
# Step 1: pngquant
########################################
log "Step 1/3: Quantization (pngquant)"
if ! pngquant \
--quality="$QUALITY" \
--speed="$SPEED" \
--strip \
--force \
--output "$tmp" \
"$input"; then
fail "pngquant failed on $input"
rm -f "$tmp"
return 1
fi
[[ -f "$tmp" ]] || { fail "pngquant produced no output for $input"; return 1; }
########################################
# Step 2: oxipng
########################################
log "Step 2/3: Optimization (oxipng)"
if ! oxipng \
-o "$OXI_LEVEL" \
--strip all \
--quiet \
--out "$out" \
"$tmp"; then
fail "oxipng failed on $input"
rm -f "$tmp" "$out"
return 1
fi
rm -f "$tmp"
[[ -f "$out" ]] || { fail "oxipng produced no output for $input"; return 1; }
########################################
# Step 3: Size check + reporting
########################################
log "Step 3/3: Size validation"
local orig_size new_size saved pct
orig_size=$(stat -f%z "$input") || { fail "Failed to read size of $input"; return 1; }
new_size=$(stat -f%z "$out") || { fail "Failed to read size of $out"; return 1; }
# Human-readable formatter
format_size() {
local bytes=$1
awk '
function human(x) {
s="B KB MB GB TB PB"
split(s,arr)
for(i=1; x>=1024 && i<length(arr); i++) x/=1024
return sprintf("%.2f %s", x, arr[i])
}
BEGIN { print human('"$bytes"') }
'
}
# If keeping only smaller outputs
if [[ "$KEEP_IF_LARGER" -eq 1 && $new_size -ge $orig_size ]]; then
warn "Output larger than original, discarding"
rm -f "$out"
return 0
fi
saved=$((orig_size - new_size))
# Avoid division by zero
if (( orig_size > 0 )); then
pct=$(awk "BEGIN { printf \"%.1f\", ($saved/$orig_size)*100 }")
else
pct="0.0"
fi
log "✔ Success: $out"
printf " Size: %-10s → %-10s (↓%s%%)\n" \
"$(format_size "$orig_size")" \
"$(format_size "$new_size")" \
"$pct"
}
########################################
# Execution
########################################
if [[ "$JOBS" -gt 1 ]]; then
export -f process_file log warn fail
printf "%s\0" "${FILES[@]}" | xargs -0 -P "$JOBS" -I{} bash -c 'process_file "$@"' _ {}
else
for f in "${FILES[@]}"; do
process_file "$f"
done
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment