Skip to content

Instantly share code, notes, and snippets.

@stringertheory
Last active July 19, 2026 01:43
Show Gist options
  • Select an option

  • Save stringertheory/6ed8702b2c0fcf1819535b8602d3b616 to your computer and use it in GitHub Desktop.

Select an option

Save stringertheory/6ed8702b2c0fcf1819535b8602d3b616 to your computer and use it in GitHub Desktop.
Save a macOS clipboard image as a PNG
#!/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