Last active
November 17, 2025 12:05
-
-
Save cheeseonamonkey/da365acc2baa4363c6e2254f70f56233 to your computer and use it in GitHub Desktop.
re-compress apk
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
| #!/bin/bash | |
| recompress_apk() { | |
| local dry_run=0 png_opt=6 jpg_q=70 zip_lv=9 webp_q=85 use_webp=0 use_7z=0 zopfli_i=50 advzip_i=100 lossy_png=0 | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -d|--dry-run) dry_run=1; shift ;; | |
| -p|--png-opt) png_opt=$2; shift 2 ;; | |
| -j|--jpg-quality) jpg_q=$2; shift 2 ;; | |
| -z|--zip-level) zip_lv=$2; shift 2 ;; | |
| -w|--webp) use_webp=1; webp_q=${2:-85}; shift $([[ $2 =~ ^[0-9]+$ ]] && echo 2 || echo 1) ;; | |
| -7|--7zip) use_7z=1; shift ;; | |
| --zopfli-iter) zopfli_i=$2; shift 2 ;; | |
| --advzip-iter) advzip_i=$2; shift 2 ;; | |
| --lossy-png) lossy_png=1; shift ;; | |
| -h|--help) cat << 'EOF' | |
| Usage: recompress_apk [FLAGS] <apk> [keystore] | |
| FLAGS: | |
| -d, --dry-run Stop before repackaging (test compression) | |
| -p, --png-opt N PNG level 0-6 (default: 6, oxipng) | |
| -j, --jpg-quality N JPEG quality 0-100 (default: 70) | |
| -w, --webp [Q] Convert to WebP (default: 85) | |
| -z, --zip-level N ZIP compression 0-9 (default: 9) | |
| -7, --7zip Use 7z ultra (SLOW, 2-5% better) | |
| --zopfli-iter N Zopfli iterations (default: 50) | |
| --advzip-iter N Advzip iterations (default: 100) | |
| --lossy-png Palette reduction (pngquant, lossy) | |
| EXAMPLES: | |
| recompress_apk app.apk # Standard (strip+optimize) | |
| recompress_apk -w 80 app.apk # WebP @ 80 quality | |
| recompress_apk -j 60 -p 6 --zopfli-iter 100 app.apk # Aggressive lossless | |
| recompress_apk --lossy-png -w 70 -7 app.apk # Maximum lossy compression | |
| recompress_apk -7 --advzip-iter 200 app.apk # Release build quality | |
| EXTREME: recompress_apk --lossy-png -w 75 -7 --zopfli-iter 100 --advzip-iter 200 app.apk | |
| EOF | |
| return 0 ;; | |
| -*) echo "Error: Unknown flag $1"; return 1 ;; | |
| *) break ;; | |
| esac | |
| done | |
| [[ $# -lt 1 ]] && { echo "Usage: recompress_apk [FLAGS] <apk> [keystore]"; return 1; } | |
| local apk=$1 keystore=$2 | |
| [[ ! -f $apk ]] && { echo "Error: APK not found"; return 1; } | |
| echo "=== Tool Check ===" | |
| for t in zopflipng:zopfli oxipng jpegoptim cwebp:webp pngquant advzip:advancecomp 7z strip zipalign apksigner; do | |
| IFS=: read -r cmd pkg <<< "$t" | |
| command -v "$cmd" &>/dev/null && echo "✓ $cmd" || echo "✗ $cmd${pkg:+ ($pkg)}" | |
| done; echo | |
| # Keystore handling | |
| if [[ -z $keystore ]]; then | |
| keystore=${RECOMPRESS_KEYSTORE:-} | |
| [[ -z $keystore ]] && read -rp "Keystore: " keystore || echo "Cached keystore" | |
| export RECOMPRESS_KEYSTORE=$keystore | |
| fi | |
| [[ ! -f $keystore ]] && { unset RECOMPRESS_KEYSTORE; echo "Error: Keystore not found"; return 1; } | |
| # Password handling | |
| if [[ -z $RECOMPRESS_PASSWORD ]]; then | |
| local cfg=$(dirname "$keystore")/config.yml | |
| [[ -f $cfg ]] && RECOMPRESS_PASSWORD=$(awk '/keystorepass:/{gsub(/"/, "", $2); print $2}' "$cfg") | |
| [[ -z $RECOMPRESS_PASSWORD ]] && read -rsp "Password: " RECOMPRESS_PASSWORD && echo || echo "Cached password" | |
| fi | |
| export RECOMPRESS_PASSWORD | |
| local tmp="/tmp/rc_$$" zip="/tmp/rc_$$.zip" out="${apk%.apk}_aligned.apk" orig=$(stat -c%s "$apk") | |
| trap "rm -rf '$tmp' '$zip'" EXIT | |
| delta() { awk -v p=$1 -v c=$2 'BEGIN{s=p-c;printf" -%.2fMB (-%.1f%%)\n",s/1048576,s/p*100}'; } | |
| echo "Extracting..."; mkdir -p "$tmp" && unzip -q "$apk" -d "$tmp" || return 1 | |
| local sz=$(du -sb "$tmp"|awk '{print $1}') | |
| echo -n "Removing META-INF..." | |
| rm -rf "$tmp/META-INF" | |
| local new_sz=$(du -sb "$tmp"|awk '{print $1}') | |
| delta "$sz" "$new_sz" | |
| sz=$new_sz | |
| # Clean junk files | |
| echo -n "Cleaning junk..." | |
| local junk=0 | |
| while IFS= read -r -d '' f; do junk=$((junk+$(stat -c%s "$f"))); rm "$f"; done < <(find "$tmp" -type f \( -name "*.md" -o -name "*.txt" -o -name NOTICE -o -name LICENSE -o -name "*.pro" -o -name "*.properties" \) -print0) | |
| find "$tmp" -type d -name __MACOSX -exec rm -rf {} + 2>/dev/null | |
| [[ $junk -gt 0 ]] && awk -v s=$junk 'BEGIN{printf" -%.2fMB\n",s/1048576}' || echo " 0B" | |
| # Strip native libs | |
| echo "Stripping libs..." | |
| local strip_s=0 libs=0 | |
| while IFS= read -r -d '' f; do | |
| local b=$(stat -c%s "$f"); strip --strip-all "$f" 2>/dev/null || true | |
| strip_s=$((strip_s+b-$(stat -c%s "$f"))); echo -ne "\r $((++libs)) libs" | |
| done < <(find "$tmp" -type f -name "*.so" -print0) | |
| [[ $libs -gt 0 ]] && echo -e "\r $libs libs: -$(awk -v s=$strip_s 'BEGIN{printf"%.2fMB",s/1048576}')" | |
| # Image compression | |
| if [[ $use_webp -eq 1 ]]; then | |
| command -v cwebp &>/dev/null || { echo "WARNING: cwebp missing"; } | |
| echo "WebP (q=$webp_q)..." | |
| local saved=0 cnt=0 | |
| while IFS= read -r -d '' f; do | |
| local b=$(stat -c%s "$f") | |
| cwebp -q $webp_q -m 6 -mt "$f" -o "${f%.*}.webp" &>/dev/null && rm "$f" | |
| saved=$((saved+b-$(stat -c%s "${f%.*}.webp" 2>/dev/null||echo 0))) | |
| echo -ne "\r $((++cnt))" | |
| done < <(find "$tmp" -type f \( -iname "*.png" -o -iname "*.jpg" -o -iname "*.jpeg" \) -print0) | |
| [[ $cnt -gt 0 ]] && echo -e "\r $cnt: -$(awk -v s=$saved 'BEGIN{printf"%.2fMB",s/1048576}')" | |
| else | |
| echo "Compressing..." | |
| local png_s=0 jpg_s=0 pngs=0 jpgs=0 tool="" | |
| # PNG compression (priority: pngquant > zopflipng > oxipng) | |
| if [[ $lossy_png -eq 1 ]] && command -v pngquant &>/dev/null; then | |
| tool="pngquant" | |
| while IFS= read -r -d '' f; do | |
| local b=$(stat -c%s "$f") | |
| pngquant --force --ext .png --quality 50-85 --speed 1 "$f" 2>/dev/null || true | |
| png_s=$((png_s+b-$(stat -c%s "$f"))); echo -ne "\r PNG:$((++pngs))" | |
| done < <(find "$tmp" -type f -iname "*.png" -print0) | |
| elif command -v zopflipng &>/dev/null; then | |
| tool="zopflipng" | |
| while IFS= read -r -d '' f; do | |
| local b=$(stat -c%s "$f") | |
| zopflipng -y --iterations=$zopfli_i --lossy_transparent --lossy_8bit "$f" "$f" 2>/dev/null || true | |
| png_s=$((png_s+b-$(stat -c%s "$f"))); echo -ne "\r PNG:$((++pngs))" | |
| done < <(find "$tmp" -type f -iname "*.png" -print0) | |
| elif command -v oxipng &>/dev/null; then | |
| tool="oxipng" | |
| while IFS= read -r -d '' f; do | |
| local b=$(stat -c%s "$f") | |
| oxipng -q -o $png_opt --fix "$f" 2>/dev/null || true | |
| png_s=$((png_s+b-$(stat -c%s "$f"))); echo -ne "\r PNG:$((++pngs))" | |
| done < <(find "$tmp" -type f -iname "*.png" -print0) | |
| fi | |
| # JPG compression | |
| if command -v jpegoptim &>/dev/null; then | |
| while IFS= read -r -d '' f; do | |
| local b=$(stat -c%s "$f") | |
| jpegoptim -q -m $jpg_q --strip-all "$f" 2>/dev/null || true | |
| jpg_s=$((jpg_s+b-$(stat -c%s "$f"))); echo -ne "\r JPG:$((++jpgs))" | |
| done < <(find "$tmp" -type f \( -iname "*.jpg" -o -iname "*.jpeg" \) -print0) | |
| fi | |
| [[ -n $tool ]] && echo -ne "\r $tool " | |
| [[ $pngs -gt 0 ]] && echo -e "\r PNG: $pngs (-$(awk -v s=$png_s 'BEGIN{printf"%.2fMB",s/1048576}'))" | |
| [[ $jpgs -gt 0 ]] && echo " JPG: $jpgs (-$(awk -v s=$jpg_s 'BEGIN{printf"%.2fMB",s/1048576}'))" | |
| fi | |
| new_sz=$(du -sb "$tmp"|awk '{print $1}') | |
| echo -n "Total compression:" | |
| delta "$sz" "$new_sz" | |
| sz=$new_sz | |
| [[ $dry_run -eq 1 ]] && { echo "[dry-run] done"; return 0; } | |
| # Repackage | |
| echo -n "Re-zipping..." | |
| if [[ $use_7z -eq 1 ]] && command -v 7z &>/dev/null; then | |
| echo -n " (7z ultra)" | |
| (cd "$tmp" && 7z a -tzip -mx=9 -mfb=273 -mpass=15 "$zip" . &>/dev/null) || return 1 | |
| else | |
| (cd "$tmp" && zip -q -r -$zip_lv "$zip" .) || return 1 | |
| fi | |
| local zip_sz=$(stat -c%s "$zip") | |
| delta "$sz" "$zip_sz" | |
| # Advzip post-processing | |
| if command -v advzip &>/dev/null; then | |
| echo -n " advzip($advzip_i)..." | |
| advzip -z -4 -i $advzip_i "$zip" 2>&1 | grep -v "^$" || true | |
| local new_zip=$(stat -c%s "$zip") | |
| [[ $new_zip -lt $zip_sz ]] && awk -v b=$zip_sz -v a=$new_zip 'BEGIN{printf" -%.2fMB (-%.1f%%)\n",(b-a)/1048576,(b-a)/b*100}' || echo "" | |
| zip_sz=$new_zip | |
| fi | |
| # Align & sign | |
| echo -n "Aligning..." | |
| zipalign -f -p 4 "$zip" "$out" 2>&1|grep -v "^Verification" || return 1 | |
| local aligned_sz=$(stat -c%s "$out") | |
| awk -v z=$zip_sz -v a=$aligned_sz 'BEGIN{d=a-z;printf" %s%.2fMB\n",d>0?"+":"-",d<0?-d/1048576:d/1048576}' | |
| echo "Signing..." | |
| apksigner sign --ks "$keystore" --ks-pass "pass:$RECOMPRESS_PASSWORD" --v2-signing-enabled --min-sdk-version 1 "$out" 2>/dev/null || \ | |
| { echo "Error: Sign failed"; unset RECOMPRESS_KEYSTORE RECOMPRESS_PASSWORD; return 1; } | |
| echo "Validating..." | |
| apksigner verify "$out" 2>&1|grep -qE "(Verified|ERROR)" || { echo "Error: Invalid"; rm "$out"; return 1; } | |
| # Test install | |
| echo "Test install..." | |
| if command -v adb &>/dev/null && adb devices|grep -q "device$"; then | |
| adb install -r "$out" 2>&1|grep -q "Success" && echo " OK (on device)" || echo " FAIL" | |
| else | |
| echo " Skip (no device)" | |
| fi | |
| awk -v o=$orig -v f=$(stat -c%s "$out") -v n="$out" 'BEGIN{s=o-f;printf"\n=== FINAL ===\n%s\n %.2fMB -> %.2fMB (-%.2fMB / -%.1f%%)\n",n,o/1048576,f/1048576,s/1048576,s/o*100}' | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment