#!/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 # Contributors: dacav # 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