Last active
July 19, 2026 01:43
-
-
Save stringertheory/6ed8702b2c0fcf1819535b8602d3b616 to your computer and use it in GitHub Desktop.
Save a macOS clipboard image as a PNG
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 | |
| # Save a clipboard image using pngpaste, with optional oxipng optimization. | |
| # | |
| # Exit statuses: | |
| # 0 Image saved successfully | |
| # 1 Image could not be saved or the operation was cancelled | |
| # 2 Invalid command-line usage | |
| emulate -L zsh | |
| setopt ERR_RETURN NO_UNSET PIPE_FAIL | |
| readonly PROGRAM_NAME="${0:t}" | |
| readonly DEFAULT_FILENAME="screenshot.png" | |
| mode="prompt" | |
| filename="$DEFAULT_FILENAME" | |
| filename_was_set=0 | |
| optimize=0 | |
| optimization_level=2 | |
| optimization_level_was_set=0 | |
| usage() { | |
| cat <<EOF | |
| Usage: $PROGRAM_NAME [OPTIONS] [FILENAME] | |
| Save the image currently on the clipboard as a PNG. | |
| If FILENAME is omitted, the image is saved as: | |
| $DEFAULT_FILENAME | |
| Existing-file behavior: | |
| -p, --prompt Ask whether to overwrite, number, or cancel | |
| This is the default. | |
| -n, --number Choose the first available numbered filename, | |
| such as screenshot-1.png. | |
| -f, --force | |
| --clobber Overwrite the existing file. | |
| Other options: | |
| --optimize Losslessly optimize with oxipng | |
| --optimization-level LEVEL | |
| Set oxipng level: 0-6 or max (default: 2; | |
| requires --optimize) | |
| -h, --help Show this help. | |
| Examples: | |
| $PROGRAM_NAME | |
| $PROGRAM_NAME diagram.png | |
| $PROGRAM_NAME --number diagram.png | |
| $PROGRAM_NAME --force screenshot.png | |
| $PROGRAM_NAME --optimize --optimization-level 4 polished-capture.png | |
| $PROGRAM_NAME -- -leading-dash.png | |
| EOF | |
| } | |
| error() { | |
| print -u2 -- "$PROGRAM_NAME: $*" | |
| } | |
| numbered_filename() { | |
| local original="$1" | |
| local directory="${original:h}" | |
| local basename="${original:t}" | |
| local stem extension candidate | |
| local number=1 | |
| # Preserve the extension, if there is one. | |
| if [[ "$basename" == *.* && "$basename" != .* ]]; then | |
| stem="${basename:r}" | |
| extension=".${basename:e}" | |
| else | |
| stem="$basename" | |
| extension="" | |
| fi | |
| while true; do | |
| candidate="$directory/${stem}-${number}${extension}" | |
| if [[ ! -e "$candidate" ]]; then | |
| print -r -- "$candidate" | |
| return 0 | |
| fi | |
| (( number++ )) | |
| done | |
| } | |
| prompt_for_existing_file() { | |
| local existing="$1" | |
| local response | |
| while true; do | |
| # The function is called through command substitution, so send the | |
| # interactive prompt to stderr and reserve stdout for the result. | |
| print -u2 -n -- "'$existing' already exists. [o]verwrite, [n]umber, or [c]ancel? " | |
| read -r response | |
| case "${response:l}" in | |
| o|overwrite) | |
| print -r -- "overwrite" | |
| return 0 | |
| ;; | |
| n|number) | |
| print -r -- "number" | |
| return 0 | |
| ;; | |
| c|cancel|"") | |
| print -r -- "cancel" | |
| return 0 | |
| ;; | |
| *) | |
| print -u2 -- "Please enter o, n, or c." | |
| ;; | |
| esac | |
| done | |
| } | |
| # Parse arguments. | |
| while (( $# > 0 )); do | |
| case "$1" in | |
| -p|--prompt) | |
| mode="prompt" | |
| ;; | |
| -n|--number) | |
| mode="number" | |
| ;; | |
| -f|--force|--clobber) | |
| mode="clobber" | |
| ;; | |
| --optimize) | |
| optimize=1 | |
| ;; | |
| --optimization-level) | |
| if (( $# < 2 )); then | |
| error "--optimization-level requires a value" | |
| usage >&2 | |
| exit 2 | |
| fi | |
| if [[ "$2" != [0-6] && "$2" != "max" ]]; then | |
| error "invalid optimization level: $2 (expected 0-6 or max)" | |
| exit 2 | |
| fi | |
| optimization_level="$2" | |
| optimization_level_was_set=1 | |
| shift | |
| ;; | |
| -h|--help) | |
| usage | |
| exit 0 | |
| ;; | |
| --) | |
| shift | |
| if (( $# > 1 )); then | |
| error "too many filenames" | |
| usage >&2 | |
| exit 2 | |
| fi | |
| if (( $# == 1 )); then | |
| filename="$1" | |
| filename_was_set=1 | |
| fi | |
| break | |
| ;; | |
| -*) | |
| error "unknown option: $1" | |
| usage >&2 | |
| exit 2 | |
| ;; | |
| *) | |
| if (( filename_was_set )); then | |
| error "too many filenames" | |
| usage >&2 | |
| exit 2 | |
| fi | |
| filename="$1" | |
| filename_was_set=1 | |
| ;; | |
| esac | |
| shift | |
| done | |
| if (( optimization_level_was_set && ! optimize )); then | |
| error "--optimization-level requires --optimize" | |
| exit 2 | |
| fi | |
| if ! command -v pngpaste >/dev/null 2>&1; then | |
| error "pngpaste is not installed" | |
| error "install it with: brew install pngpaste" | |
| exit 1 | |
| fi | |
| if (( optimize )) && ! command -v oxipng >/dev/null 2>&1; then | |
| error "oxipng is not installed" | |
| error "install it with: brew install oxipng" | |
| exit 1 | |
| fi | |
| if [[ -z "$filename" ]]; then | |
| error "filename cannot be empty" | |
| exit 2 | |
| fi | |
| # Add .png when the final path component has no extension. A leading dot by | |
| # itself does not make a filename an extension-bearing filename. | |
| basename="${filename:t}" | |
| if [[ "$basename" != *.* || "$basename" == .* && "${basename#*.}" != *.* ]]; then | |
| filename="${filename}.png" | |
| fi | |
| # Normalize the directory enough to handle a bare filename cleanly. | |
| directory="${filename:h}" | |
| if [[ ! -d "$directory" ]]; then | |
| error "directory does not exist: $directory" | |
| exit 1 | |
| fi | |
| if [[ ! -w "$directory" ]]; then | |
| error "directory is not writable: $directory" | |
| exit 1 | |
| fi | |
| if [[ -d "$filename" ]]; then | |
| error "destination is a directory: $filename" | |
| exit 1 | |
| fi | |
| if [[ -e "$filename" ]]; then | |
| case "$mode" in | |
| clobber) | |
| ;; | |
| number) | |
| filename="$(numbered_filename "$filename")" | |
| ;; | |
| prompt) | |
| choice="$(prompt_for_existing_file "$filename")" | |
| case "$choice" in | |
| overwrite) | |
| ;; | |
| number) | |
| filename="$(numbered_filename "$filename")" | |
| ;; | |
| cancel) | |
| print -- "Not saved." | |
| exit 1 | |
| ;; | |
| esac | |
| ;; | |
| esac | |
| fi | |
| # Write to a temporary file first. This prevents an existing destination from | |
| # being damaged if pngpaste fails because the clipboard does not contain an | |
| # image. | |
| temporary_file="$(mktemp "$directory/.clipimg.XXXXXX.png")" || { | |
| error "could not create a temporary file in $directory" | |
| exit 1 | |
| } | |
| cleanup() { | |
| rm -f -- "$temporary_file" | |
| } | |
| trap cleanup EXIT INT TERM HUP | |
| if ! pngpaste "$temporary_file"; then | |
| error "the clipboard does not contain a supported image" | |
| exit 1 | |
| fi | |
| if (( optimize )); then | |
| if ! oxipng -o "$optimization_level" "$temporary_file" >/dev/null; then | |
| error "could not optimize the clipboard image" | |
| exit 1 | |
| fi | |
| fi | |
| if ! mv -f -- "$temporary_file" "$filename"; then | |
| error "could not save image as: $filename" | |
| exit 1 | |
| fi | |
| # The temporary path no longer exists, so disable unnecessary cleanup. | |
| trap - EXIT INT TERM HUP | |
| print -- "Saved $filename" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment