Last active
December 23, 2025 23:37
-
-
Save RexYuan/3147ffa0bb84766d1c6253781ffa6660 to your computer and use it in GitHub Desktop.
Upscale an image for print at a specified physical size and DPI using Real-ESRGAN for AI upscaling.
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 zsh | |
| # | |
| # to-dpi.zsh | |
| # | |
| # Upscale an image for print at a specified physical size and DPI | |
| # using Real-ESRGAN for AI upscaling. | |
| # | |
| # Notes: This script was vibe-coded but checked in-head by me. | |
| # | |
| # Strategy: | |
| # - Compute required pixel dimensions from mm and DPI | |
| # - Compute total required linear scale | |
| # - Perform multi-pass Real-ESRGAN upscaling (>=2x only) | |
| # - REQUIRE Real-ESRGAN to produce an output file (abort if not) | |
| # - Perform final Lanczos resize to exact target | |
| # - Write DPI metadata (no resampling) | |
| # | |
| # Usage: | |
| # [DEBUG=1] ./to-dpi.zsh <image> <dpi> <widthxheight_mm> [--dry-run] | |
| # | |
| # Examples: | |
| # ./to-dpi.zsh input.png 300 416x578 | |
| # DEBUG=1 ./to-dpi.zsh input.tiff 150 1000x1000 | |
| # | |
| set -o errexit | |
| set -o pipefail | |
| # ------------------------- | |
| # Resolve script directory | |
| # ------------------------- | |
| SCRIPT_DIR="${0:A:h}" | |
| REALESRGAN_BIN="${SCRIPT_DIR}/realesrgan-ncnn-vulkan" | |
| # ------------------------- | |
| # Argument parsing | |
| # ------------------------- | |
| if (( $# < 3 || $# > 4 )); then | |
| echo "usage: [DEBUG=1] to-dpi.zsh <image> <dpi> <widthxheight_mm> [--dry-run]" | |
| exit 1 | |
| fi | |
| input="$1" | |
| dpi="$2" | |
| size_mm="$3" | |
| mode="$4" | |
| DRY_RUN=0 | |
| [[ "$mode" == "--dry-run" ]] && DRY_RUN=1 | |
| [[ -n "$mode" && "$mode" != "--dry-run" ]] && { | |
| echo "to-dpi: unknown option '$mode'" | |
| exit 1 | |
| } | |
| # ------------------------- | |
| # Validation | |
| # ------------------------- | |
| [[ ! -f "$input" ]] && { echo "to-dpi: '$input' not found"; exit 1 } | |
| [[ ! "$dpi" =~ '^[0-9]+$' || "$dpi" -le 0 ]] && { | |
| echo "to-dpi: invalid dpi '$dpi'" | |
| exit 1 | |
| } | |
| [[ ! "$size_mm" =~ '^[0-9]+x[0-9]+$' ]] && { | |
| echo "to-dpi: invalid size '$size_mm' (expected WIDTHxHEIGHT in mm)" | |
| exit 1 | |
| } | |
| command -v magick >/dev/null || { | |
| echo "to-dpi: ImageMagick (magick) not found" | |
| exit 1 | |
| } | |
| [[ ! -x "$REALESRGAN_BIN" ]] && { | |
| echo "to-dpi: Real-ESRGAN binary not found at:" | |
| echo " $REALESRGAN_BIN" | |
| exit 1 | |
| } | |
| # ------------------------- | |
| # Parse size | |
| # ------------------------- | |
| w_mm="${size_mm%x*}" | |
| h_mm="${size_mm#*x}" | |
| # ------------------------- | |
| # Target pixels | |
| # ------------------------- | |
| tgt_w=$(python3 - <<EOF | |
| import math | |
| print(math.ceil(($w_mm / 25.4) * $dpi)) | |
| EOF | |
| ) | |
| tgt_h=$(python3 - <<EOF | |
| import math | |
| print(math.ceil(($h_mm / 25.4) * $dpi)) | |
| EOF | |
| ) | |
| # ------------------------- | |
| # Source pixels | |
| # ------------------------- | |
| read src_w src_h <<<"$(magick identify -format '%w %h' "$input")" | |
| required_scale=$(python3 - <<EOF | |
| print(max($tgt_w / $src_w, $tgt_h / $src_h)) | |
| EOF | |
| ) | |
| echo "Math:" | |
| printf " dpi = %d\n" "$dpi" | |
| printf " target_px = %dx%d\n" "$tgt_w" "$tgt_h" | |
| printf " required_scale = %.2fx\n" "$required_scale" | |
| (( $(echo "$required_scale > 32" | bc -l) )) && { | |
| echo "to-dpi: required scale too large (${required_scale}x)" | |
| exit 1 | |
| } | |
| # ------------------------- | |
| # Scaling loop | |
| # ------------------------- | |
| cur="$input" | |
| cur_w="$src_w" | |
| cur_h="$src_h" | |
| remaining_scale="$required_scale" | |
| base="${input%.*}" | |
| pass=1 | |
| while (( $(echo "$remaining_scale >= 2.0" | bc -l) )); do | |
| ai_scale=$(python3 - <<EOF | |
| import math | |
| print(min(4, int(math.floor($remaining_scale)))) | |
| EOF | |
| ) | |
| (( ai_scale < 2 )) && break | |
| printf "\nPass %d (Real-ESRGAN):\n" "$pass" | |
| printf " ai_scale = %dx\n" "$ai_scale" | |
| printf " before = %dx%d\n" "$cur_w" "$cur_h" | |
| tmp="${base}__up${pass}.png" | |
| if (( ! DRY_RUN )); then | |
| if [[ -n "$DEBUG" ]]; then | |
| "$REALESRGAN_BIN" -i "$cur" -o "$tmp" \ | |
| -n realesr-animevideov3 -s "$ai_scale" | |
| else | |
| "$REALESRGAN_BIN" -i "$cur" -o "$tmp" \ | |
| -n realesr-animevideov3 -s "$ai_scale" \ | |
| >/dev/null 2>&1 | |
| fi | |
| fi | |
| # STRICT: output must exist | |
| [[ ! -f "$tmp" ]] && { | |
| echo "to-dpi: Real-ESRGAN produced no output file" | |
| echo "to-dpi: input format likely unsupported — aborting" | |
| exit 1 | |
| } | |
| cur="$tmp" | |
| cur_w=$(( cur_w * ai_scale )) | |
| cur_h=$(( cur_h * ai_scale )) | |
| remaining_scale=$(python3 - <<EOF | |
| print(max($tgt_w / $cur_w, $tgt_h / $cur_h)) | |
| EOF | |
| ) | |
| printf " after = %dx%d\n" "$cur_w" "$cur_h" | |
| printf " remaining = %.2fx\n" "$remaining_scale" | |
| (( pass++ )) | |
| done | |
| # ------------------------- | |
| # Final resize | |
| # ------------------------- | |
| printf "\nFinal resize (Lanczos, non-AI):\n" | |
| printf " from = %dx%d\n" "$cur_w" "$cur_h" | |
| printf " to = %dx%d\n" "$tgt_w" "$tgt_h" | |
| (( DRY_RUN )) && { | |
| echo "\n(dry-run: no files written)" | |
| exit 0 | |
| } | |
| final="${base}__final.png" | |
| magick "$cur" -resize "${tgt_w}x${tgt_h}" -filter Lanczos "$final" | |
| output="${base}_${dpi}dpi.png" | |
| magick "$final" -units PixelsPerInch -density "$dpi" "$output" | |
| # ------------------------- | |
| # Cleanup (zsh-safe) | |
| # ------------------------- | |
| ups=( "${base}"__up*.png(N) ) | |
| (( ${#ups[@]} )) && rm -f -- "${ups[@]}" | |
| rm -f -- "$final" | |
| echo "\nOutput:" | |
| echo " $output" |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Download the xinntao/Real-ESRGAN-ncnn-vulkan binary here