Last active
June 20, 2026 06:02
-
-
Save yetimdasturchi/063eee362ed0b2dbcbd010ece9e8a90d to your computer and use it in GitHub Desktop.
Create scanned looking PDFs with adjustable DPI/quality support
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 | |
| set -Eeuo pipefail | |
| SCRIPT_NAME="$(basename "$0")" | |
| INPUT="" | |
| OUTPUT="scanned_output.pdf" | |
| DPI="130" | |
| QUALITY="65" | |
| INSTALL_DEPS="false" | |
| usage() { | |
| cat <<USAGE | |
| Usage: | |
| ./$SCRIPT_NAME [options] input.pdf [output.pdf] | |
| Options: | |
| -d, --dpi NUMBER Output resolution. Default: 130 | |
| -q, --quality NUMBER JPEG quality from 1 to 100. Default: 65 | |
| --install Try to install missing dependencies, then continue | |
| -h, --help Show this help message | |
| Examples: | |
| ./$SCRIPT_NAME input.pdf output.pdf | |
| ./$SCRIPT_NAME --dpi 130 --quality 65 input.pdf output.pdf | |
| ./$SCRIPT_NAME -d 150 -q 72 input.pdf scanned.pdf | |
| ./$SCRIPT_NAME --install input.pdf output.pdf | |
| USAGE | |
| } | |
| error() { | |
| echo "Error: $*" >&2 | |
| } | |
| info() { | |
| echo "$*" | |
| } | |
| is_number() { | |
| [[ "$1" =~ ^[0-9]+$ ]] | |
| } | |
| parse_args() { | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -d|--dpi) | |
| [[ $# -ge 2 ]] || { error "Missing value for $1"; exit 1; } | |
| DPI="$2" | |
| shift 2 | |
| ;; | |
| --dpi=*) | |
| DPI="${1#*=}" | |
| shift | |
| ;; | |
| -q|--quality) | |
| [[ $# -ge 2 ]] || { error "Missing value for $1"; exit 1; } | |
| QUALITY="$2" | |
| shift 2 | |
| ;; | |
| --quality=*) | |
| QUALITY="${1#*=}" | |
| shift | |
| ;; | |
| --install) | |
| INSTALL_DEPS="true" | |
| shift | |
| ;; | |
| -h|--help) | |
| usage | |
| exit 0 | |
| ;; | |
| --) | |
| shift | |
| break | |
| ;; | |
| -* ) | |
| error "Unknown option: $1" | |
| usage | |
| exit 1 | |
| ;; | |
| *) | |
| if [[ -z "$INPUT" ]]; then | |
| INPUT="$1" | |
| elif [[ "$OUTPUT" == "scanned_output.pdf" ]]; then | |
| OUTPUT="$1" | |
| else | |
| error "Too many arguments: $1" | |
| usage | |
| exit 1 | |
| fi | |
| shift | |
| ;; | |
| esac | |
| done | |
| while [[ $# -gt 0 ]]; do | |
| if [[ -z "$INPUT" ]]; then | |
| INPUT="$1" | |
| elif [[ "$OUTPUT" == "scanned_output.pdf" ]]; then | |
| OUTPUT="$1" | |
| else | |
| error "Too many arguments: $1" | |
| usage | |
| exit 1 | |
| fi | |
| shift | |
| done | |
| } | |
| validate_args() { | |
| if [[ -z "$INPUT" ]]; then | |
| usage | |
| exit 1 | |
| fi | |
| if [[ ! -f "$INPUT" ]]; then | |
| error "Input file not found: '$INPUT'" | |
| exit 1 | |
| fi | |
| if ! is_number "$DPI" || [[ "$DPI" -lt 72 || "$DPI" -gt 600 ]]; then | |
| error "DPI must be a number between 72 and 600. Current value: $DPI" | |
| exit 1 | |
| fi | |
| if ! is_number "$QUALITY" || [[ "$QUALITY" -lt 1 || "$QUALITY" -gt 100 ]]; then | |
| error "Quality must be a number between 1 and 100. Current value: $QUALITY" | |
| exit 1 | |
| fi | |
| } | |
| missing_dependencies() { | |
| local missing=() | |
| command -v pdftoppm >/dev/null 2>&1 || missing+=("pdftoppm") | |
| command -v img2pdf >/dev/null 2>&1 || missing+=("img2pdf") | |
| if ! command -v magick >/dev/null 2>&1 && ! command -v convert >/dev/null 2>&1; then | |
| missing+=("imagemagick") | |
| fi | |
| printf '%s\n' "${missing[@]}" | |
| } | |
| install_command_hint() { | |
| cat <<'HINT' | |
| Install commands: | |
| Ubuntu/Debian: | |
| sudo apt update | |
| sudo apt install -y poppler-utils img2pdf imagemagick | |
| Fedora: | |
| sudo dnf install -y poppler-utils img2pdf ImageMagick | |
| Arch Linux: | |
| sudo pacman -S --needed poppler img2pdf imagemagick | |
| macOS with Homebrew: | |
| brew install poppler img2pdf imagemagick | |
| HINT | |
| } | |
| try_install_dependencies() { | |
| info "Trying to install missing dependencies..." | |
| if command -v apt-get >/dev/null 2>&1; then | |
| sudo apt-get update | |
| sudo apt-get install -y poppler-utils img2pdf imagemagick | |
| elif command -v dnf >/dev/null 2>&1; then | |
| sudo dnf install -y poppler-utils img2pdf ImageMagick | |
| elif command -v pacman >/dev/null 2>&1; then | |
| sudo pacman -S --needed poppler img2pdf imagemagick | |
| elif command -v brew >/dev/null 2>&1; then | |
| brew install poppler img2pdf imagemagick | |
| else | |
| error "No supported package manager found. Please install dependencies manually." | |
| install_command_hint | |
| exit 1 | |
| fi | |
| } | |
| check_dependencies() { | |
| local missing | |
| missing="$(missing_dependencies | tr '\n' ' ')" | |
| if [[ -z "${missing// }" ]]; then | |
| return 0 | |
| fi | |
| error "Required dependency/dependencies are not installed: $missing" | |
| if [[ "$INSTALL_DEPS" == "true" ]]; then | |
| try_install_dependencies | |
| local missing_after | |
| missing_after="$(missing_dependencies | tr '\n' ' ')" | |
| if [[ -n "${missing_after// }" ]]; then | |
| error "Still missing after install attempt: $missing_after" | |
| install_command_hint | |
| exit 1 | |
| fi | |
| info "Dependencies installed successfully." | |
| else | |
| install_command_hint | |
| echo "" | |
| echo "Or run this script with --install:" | |
| echo " ./$SCRIPT_NAME --install --dpi $DPI --quality $QUALITY '$INPUT' '$OUTPUT'" | |
| exit 1 | |
| fi | |
| } | |
| get_imagemagick_command() { | |
| if command -v magick >/dev/null 2>&1; then | |
| echo "magick" | |
| else | |
| echo "convert" | |
| fi | |
| } | |
| main() { | |
| parse_args "$@" | |
| validate_args | |
| check_dependencies | |
| local im_cmd | |
| im_cmd="$(get_imagemagick_command)" | |
| local workdir pages_dir scanned_dir | |
| workdir="$(mktemp -d)" | |
| pages_dir="$workdir/pages" | |
| scanned_dir="$workdir/scanned" | |
| cleanup() { | |
| rm -rf "$workdir" | |
| } | |
| trap cleanup EXIT | |
| mkdir -p "$pages_dir" "$scanned_dir" | |
| info "Splitting PDF into JPEG pages... DPI=$DPI, QUALITY=$QUALITY" | |
| pdftoppm -r "$DPI" "$INPUT" "$pages_dir/page" -jpeg -jpegopt quality="$QUALITY",progressive=y,optimize=y | |
| info "Applying scan effect..." | |
| local i=0 rotate output_page | |
| shopt -s nullglob | |
| for f in "$pages_dir"/page-*.jpg; do | |
| i=$((i + 1)) | |
| if [[ $((i % 2)) -eq 0 ]]; then | |
| rotate="0.35" | |
| else | |
| rotate="-0.25" | |
| fi | |
| output_page="$scanned_dir/$(basename "$f")" | |
| "$im_cmd" "$f" \ | |
| -rotate "$rotate" \ | |
| -attenuate 0.05 +noise Gaussian \ | |
| -blur 0x0.18 \ | |
| -brightness-contrast 1x5 \ | |
| -strip \ | |
| -interlace JPEG \ | |
| -sampling-factor 4:2:0 \ | |
| -quality "$QUALITY" \ | |
| "$output_page" | |
| done | |
| if [[ "$i" -eq 0 ]]; then | |
| error "No pages were generated from the input PDF." | |
| exit 1 | |
| fi | |
| info "Creating compressed PDF..." | |
| img2pdf "$scanned_dir"/page-*.jpg -o "$OUTPUT" | |
| info "Done: $OUTPUT" | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment