imgbb-uploader/imgbb
Ritchie Cunningham a4d90f5e9e [Add] Support for taking screenshots of specified monitor number.
Notes:
1) Only works with scrot currently.
2) Defining monitor_num in config overrides default operation of
   uploading image in clipboard.
3) --monitor (-M) option will override selected display in config.
2025-04-26 00:32:42 +01:00

479 lines
16 KiB
Bash
Executable File

#!/bin/bash
# -----------------------------------------------------------------------------
# Script Name: imgbb
# Description: Uploads an image from the system clipboard or a specified file
# to ImgBB (imgbb.com) using their V1 API. Retrieves the direct
# image URL, prints it to stdout, and copies it to the X11 clipboard.
# Author: Ritchie Cunningham <ritchie@ritchiecunningham.co.uk>
# Contributors: dacav <dacav@fastmail.com>
# License: GPL 3.0 License
# Version: 2.0
# Date: 22/04/2025
# -----------------------------------------------------------------------------
# === Default Configuration & Argument Parsing. ===
config_file="$HOME/.config/imgbb_uploader.conf"
imgbb_api_url="https://api.imgbb.com/1/upload"
# imgbb first reads defaults from ~/.config/imgbb_uploader.conf.
# If defaults not found in the config file, it reads them from the values
# assigned below.
# Optional flags override both config and values below.
api_key=""
filepath=""
expire_seconds=""
custom_name=""
markdown_mode="false"
org_mode="false"
select_mode="false"
monitor_num=""
screenshot_tool=""
clipboard="$XDG_SESSION_TYPE"
[ ! -e "$config_file" ] || . "$config_file"
print_usage() {
echo "Usage: $(basename "$0") [options] [filepath]"
echo "Uploads image from clipboard (default) or filepath to ImgBB."
echo ""
echo "Options:"
echo " [filepath] Optional path to an image file to upload."
echo " -e, --expire DURATION Set auto-deletion timeout. DURATION is a number"
echo " followed by s(econds), m(inutes), h(ours), or d(ays)."
echo " Default unit is minutes if unspecified."
echo " '0', 'none', 'never' to override default expiry."
echo " -n, --name NAME Set a custom filename for the uploaded image."
echo " --markdown Output/copy the URL in Markdown image format."
echo " --org Output/copy the URL in Orgmode image format."
echo " -s, --select Take screenshot of selected area for upload."
echo " -M, --monitor NUM Take screenshot of monitor NUM (e.g., 0, 1) for upload"
echo " -h, --help Show this help message."
}
die() {
declare msg
msg="$*"
notify_cmd -u critical "ImgBB Upload Error" "$msg" &>/dev/null
echo "$msg" >&2
exit 1
}
# Parse command-line options.
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-e|--expire)
expire_input="$2" # Usr input.
# Check for 'no expiry value'.
if [[ "$expire_input" == "0" || "$expire_input" == "never" || "$expire_input" == 'none' ]]; then
expire_seconds="" # Unset for no expiration.
echo "Setting NO expiration."
else
# Proceed with parsing regular.
value="" unit="" seconds=""
# Regex to capture the number part and optional unit part (s,m,h,d)
if [[ "$expire_input" =~ ^([0-9]+)([smhd]?)$ ]]; then
value="${BASH_REMATCH[1]}" # The numeric part.
unit="${BASH_REMATCH[2]}" # The unit part.
# Default to minutes if no unit was provided.
if [ -z "$unit" ]; then unit="m"; fi
# Calculate total seconds based on the unit.
case "$unit" in
s) seconds=$((value)) ;;
m) seconds=$((value * 60)) ;;
h) seconds=$((value * 3600)) ;; # 60*60.
d) seconds=$((value * 86400)) ;; # 60*60*24.
*) # Not really requried given the regex, but hey-ho.
echo "Error: Internal error parsing unit '$unit' from '$expire_input'."
exit 1 ;;
esac
# Validate against ImgBB limits (60 seconds to 15552000 seconds / 180 days).
if (( seconds < 60 )) || (( seconds > 15552000 )); then
echo "Error: Calculated expiration ($seconds seconds) is outside of ImgBB's allowed range (60s - 15552000s)." >&2
exit 1
fi
# Store the final result.
expire_seconds="$seconds"
echo "Expiration set to $expire_seconds seconds ($expire_input)"
else
# Someone did funky input. Tell them to stop that!
echo "Error: Invalid expiration format '$expire_input'." >&2
echo "Use a number optionally followed by s, m, h or d OR use 0/none/never (e.g., 300, 5m, 2h, 7d)." >&2
print_usage # Maybe they need help.
exit 1
fi
fi
shift
shift
;;
-n|--name)
custom_name="$2"
shift
shift
;;
--markdown)
markdown_mode="true"
org_mode="false" # Sorry org, only one at a time.
shift
;;
--org)
org_mode="true"
markdown_mode="false"
shift
;;
-s|--select)
select_mode="true"
shift
;;
-M|--monitor)
monitor_num="$2"
# Check if it's a non-negative integer.
if ! [[ "$monitor_num" =~ ^[0-9]+$ ]]; then
die "Error: Monitor number '$monitor_num' is not a valid non-negative integer."
fi
shift
shift
;;
-h|--help)
print_usage
exit 0
;;
-*)
# Unknown option.
echo "Error: Unknown option '$1'" >&2
print_usage
exit 1
;;
*)
# Assume it's the filepath (only one allowed).
if [ -n "$filepath" ]; then
echo "Error: Multiple filepaths provided ($filepath, $1). Only one allowed." >&2
print_usage
exit 1
fi
filepath="$1"
shift
;;
esac
done
# Validate mutually exclusive input modes.
if [ "$select_mode" = "true" ] && [ -n "$filepath" ]; then
die "Error: Cannot use --select (-s) flag and provided filepath simultaneously."
fi
if [ -n "$monitor_num" ] && [ -n "$filepath" ]; then
die "Error: Cannot use --monitor (-M) flag and provided filepath simultaneously."
fi
if [ "$select_mode" = "true" ] && [ -n "$monitor_num" ]; then
die "Error: Cannot use --select (-s) and --monitor (-M) flags simultaneously."
fi
# Check if filepath exists and is readable if provided.
if [ -n "$filepath" ] && [ ! -r "$filepath" ]; then
echo "Error: Cannot read file '$filepath'" >&2
exit 1
fi
# === Read API Key. ===
if [[ -n "$IMGBB_API_KEY" ]]; then
api_key="$IMGBB_API_KEY"
fi
# Check if API key is actually set.
if [ -z "$api_key" ]; then
# Use function defined below if notify-send exists.
notify_cmd -u critical "ImgBB Upload Error" "API Key not found." &>/dev/null
echo "Error: API Key not found." >&2
echo "Please store your key in '$config_file' or set the IMGBB_API_KEY environment variable." >&2
exit 1
fi
# === Check Dependencies. ===
check_command() {
if ! command -v "$1" &> /dev/null; then
notify_cmd -u critical "ImgBB Upload Error" "'$1' command not found. Please install it." &>/dev/null
echo "Error: '$1' command not found. Please install it (e.g., sudo apt install $1)." >&2
exit 1
fi
}
# Define notify_cmd first based on whether notify-send exists.
notify_cmd() { :; } # Default to no-up.
if ! command -v notify-send &> /dev/null; then
notify_cmd() { :; } # No-op if not found.
echo "Warning: notify-send not found. Desktop notifications disabled." >&2
else
notify_cmd() { notify-send "$@"; } # Actual command if found.
fi
# Now check other dependencies.
check_command curl
check_command jq
# Check screenshot tool if needed.
# Monitor mode currently hardcodes scrot, ensure it's checked if -M is used.
if [ -n "$monitor_num" ]; then check_command "scrot"; fi
if [ "$select_mode" = "true" ]; then
check_command "$screenshot_tool"
elif [ -z "$filepath" ]; then
# Check clipboard tool only if using clipboard mode (and not select mode).
case "$clipboard" in
x11)
check_command xclip
;;
wayland)
check_command wl-paste
;;
*)
die "Unsupported clipboard: $clipboard"
esac
fi
# === Main Logic. ===
paste_x11() {
declare tmp
tmp_img="$(mktemp)" || die "Could not create temp file."
xclip -selection clipboard -t image/png -o >"$tmp_img" 2>/dev/null
if [ -s "$tmp_img" ]; then
mv "$tmp_img" "$tmp_img.png"
printf "%s\n" "$tmp_img.png"
return 0
fi
xclip -selection clipboard -t image/jpeg -o >"$tmp_img" 2>/dev/null
if [ -s "$tmp_img" ]; then
mv "$tmp_img" "$tmp_img.jpeg"
printf "%s\n" "$tmp_img.jpeg"
return 0
fi
rm -f "$tmp_img"
die "Could not get PNG or JPEG image data from clipboard."
}
paste_wl() {
declare tmp_img
declare format
format="$(wl-paste -l | grep -m1 -F -e image/png -e image/jpeg)" ||
die "Invalid file type in clipboard."
tmp_img="$(mktemp)" || die "Could not create temp file."
if wl-paste -t "$format" >"$tmp_img" 2>/dev/null; then
mv "$tmp_img" "$tmp_img.${format##image/}"
printf "%s\n" "$tmp_img.${format##image/}"
return 0
fi
rm -f "$tmp_img"
die "Could not get PNG or JPEG image data from clipboard."
}
yank_x11() {
xclip -selection clipboard
}
yank_wl() {
wl-copy
}
TMP_IMG="" # Path to the image file to upload.
response="" # API response.
image_source_description=""
temp_file_to_clean="" # Track temp file for cleanup if created.
# === Determine Input and Upload ===
# --- Define base curl options ---
curl_opts_base=(-s -S -f -L -X POST --form "key=$api_key")
# --- Set default trap (will be cleared or modified below) ---
trap 'rm -f "$temp_file_to_clean"' EXIT
if [ "$select_mode" = "true" ]; then
# === Screenshot Mode ===
image_source_description="Screenshot selection"
notify_cmd "ImgBB Uploader" "Select area for screenshot using $screenshot_tool..."
screenshot_cmd=""
case "$screenshot_tool" in
maim) screenshot_cmd="maim -s -f png /dev/stdout";;
scrot) screenshot_cmd="scrot -s -o /dev/stdout";;
flameshot) screenshot_cmd="flameshot gui -r";;
*) die "Unsupported screenshot tool configured: '$screenshot_tool'";;
esac
# Build full options for this mode, starting with base.
curl_opts=("${curl_opts_base[@]}")
if [ -n "$expire_seconds" ]; then curl_opts_base+=(--form "expiration=$expire_seconds"); fi
if [ -n "$custom_name" ]; then curl_opts_base+=(--form "name=$custom_name"); fi
# Add mode-specific image source (stdin) and the API URL.
curl_opts+=(--form "image=@-;filename=screenshot.png")
curl_opts+=("$imgbb_api_url")
notify_cmd "ImgBB Uploader" "Uploading $image_source_description..."
# Capture the response and potential curl errors.
temp_stderr=$(mktemp)
response=$(eval "$screenshot_cmd" | curl "${curl_opts[@]}" 2>"$temp_stderr")
# Check the status of both commands in the pipeline.
pipeline_status=("${PIPESTATUS[@]}")
screenshot_status=${pipeline_status[0]}
upload_status=${pipeline_status[1]}
curl_stderr=$(cat "$temp_stderr")
rm -rf "$temp_stderr"
# Check for errors in the pipeline.
if [ "$screenshot_status" -ne 0 ]; then
die "Screenshot command ($screenshot_tool) failed (status $screenshot_status).";
fi
if (( upload_status != 0 )); then
die "curl command failed during upload (status $upload_status). Check network? Curl stderr: $curl_stderr";
fi
# Clear the trap, no temp file used in this mode.
trap '' EXIT
elif [ -n "$monitor_num" ]; then
# === Screenshot Monitor Mode ===
image_source_description="Monitor $monitor_num screenshot"
screenshot_tool="scrot" # Requires scrot for --monitor flag.
notify_cmd "ImgBB Uploader" "Capturing monitor $monitor_num using $screenshot_tool..."
# Build full options array starting with common.
curl_opts=("${curl_opts_base[@]}")
if [ -n "$expire_seconds" ]; then
curl_opts+=(--form "expiration=$expire_seconds");
fi
if [ -n "$custom_name" ]; then curl_opts+=(--form "name=$custom_name"); fi
# --- Create temp file for scrot output ---
TMP_IMG=$(mktemp --suffix=.png) || die "Could not create temp file for screenshot."
temp_file_to_clean="$TMP_IMG" # Ensure trap cleans up.
trap 'rm -f "$temp_file_to_clean"' EXIT
rm -f "$TMP_IMG"
# --- Take screenshot saving to temp file ---
if ! scrot --monitor "$monitor_num" "$TMP_IMG"; then
#rm -f "$TMP_IMG" # Clean up failed/empty file.
die "Screenshot command ($screenshot_tool --monitor $monitor_num) failed (status $?). Check monitor index."
fi
# Add temp file path to curl options
curl_opts+=(--form "image=@$TMP_IMG")
curl_opts+=("$imgbb_api_url")
notify_cmd "ImgBB Uploader" "Uploading $image_source_description..."
temp_stderr=$(mktemp)
response=$(eval "$screenshot_cmd" | curl "${curl_opts[@]}" 2>"$temp_stderr")
pipeline_status=("${PIPESTATUS[@]}")
screenshot_status=${pipeline_status[0]}
upload_status=${pipeline_status[1]}
curl_stderr=$(cat "$temp_stderr")
rm -f "$temp_stderr"
if [ "$screenshot_status" -ne 0 ]; then
die "Screenshot command ($screenshot_tool --monitor $monitor_num) failed (status $screenshot_status).";
fi
if (( upload_status != 0 )); then
die "curl command failed during upload (status $upload_status). Check network? Curl stderr: $curl_stderr";
fi
trap '' EXIT # Clear trap.
elif [ -n "$filepath" ]; then
# === File Input Mode. ===
image_source_description="file '$filepath'"
TMP_IMG="$filepath" # Use the provided filepath directly.
echo "Using image from file: $filepath."
trap '' EXIT # Clear trap if we are not using temp files.
# Build full options for this mode, starting with base.
curl_opts=("${curl_opts_base[@]}")
curl_opts+=(--form "image=@$TMP_IMG") # Use @filepath
if [ -n "$expire_seconds" ]; then curl_opts+=(--form "expiration=$expire_seconds"); fi
if [ -n "$custom_name" ]; then curl_opts+=(--form "name=$custom_name"); fi
curl_opts+=("$imgbb_api_url")
# Upload the image using curl.
notify_cmd "ImgBB Uploader" "Uploading $image_source_description..."
response=$(curl "${curl_opts[@]}")
#Check curl exit status.
if [ $? -ne 0 ]; then die "curl command failed during upload. Check network?"; fi
else
# === Clipboard Input Mode ===
image_source_description="clipboard"
# Use helper functions to get image data into temp file $TMP_IMG.
TMP_IMG=""; # Reset it just in case.
case "$clipboard" in
x11) TMP_IMG=$(paste_x11);;
wayland) TMP_IMG=$(paste_wl);;
esac || exit 1 # Exit if paste function failed.
temp_file_to_clean="$TMP_IMG" # Mark temp file for cleanup by trap.
# Ensure trap is set correctly.
trap 'rm -f "$temp_file_to_clean"' EXIT
# Build full options for this mode, starting with base.
curl_opts=("${curl_opts_base[@]}")
curl_opts+=(--form "image=@$TMP_IMG") # Upload from temp file path.
if [ -n "$expire_seconds" ]; then curl_opts+=(--form "expiration=$expire_seconds"); fi
if [ -n "$custom_name" ]; then curl_opts+=(--form "name=$custom_name"); fi
curl_opts+=("$imgbb_api_url")
# Upload the image using curl.
notify_cmd "ImgBB Uploader" "Uploading $image_source_description..."
response=$(curl "${curl_opts[@]}")
# Check curl exit status.
if [ $? -ne 0 ]; then die "curl command failed during upload. Check network?"; fi
fi
# === Process Response (This is common to all modes) ===
success=$(echo "$response" | jq -r '.success')
if [ "$success" != "true" ]; then
error_msg=$(echo "$response" | jq -r '.error.message // "Unknown API error"')
notify_cmd -u critical "ImgBB Upload Error" "API Error: $error_msg"
echo "Error: ImgBB API returned an error - $error_msg" >&2
echo "Full response: $response" >&2
exit 1
fi
# Extract the direct image URL.
image_url=$(echo "$response" | jq -r '.data.url')
# Check if URL was successfully extracted.
if [ -z "$image_url" ] || [ "$image_url" == "null" ]; then
notify_cmd -u critical "ImgBB Upload Error" "Could not parse image URL from API response."
echo "Error: Could not parse image URL from API response." >&2
echo "Full response: $response" >&2
exit 1
fi
# Format output based on markdown flag.
output_url="$image_url"
if [ "$markdown_mode" = "true" ]; then
# Basic markdown image syntax - assumes filename is not needed for alt text here.
output_url="![](${image_url})"
echo "Formatting as Markdown."
elif [ "$org_mode" = "true" ]; then
output_url="[[${image_url}][]]"
echo "Formatting as orgmode."
fi
# Output the URL to terminal and copy to clipboard.
echo "Image URL:"
echo "$output_url"
# Copy URL only if we determined we are not uploading from file (i.e., we used clipboard input)
# Or always copy? Let's always copy for now.
case "$clipboard" in
x11) printf "%s\n" "$output_url" | yank_x11;;
wayland) printf "%s\n" "$output_url" | yank_wl;;
esac
notify_cmd "ImgBB Uploader" "Success! URL copied: $output_url"
# Temporary file (if used) is removed automatically by the 'trap' command on exit.
exit 0