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.
479 lines
16 KiB
Bash
Executable File
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=""
|
|
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
|