mirror of
https://github.com/EdenQwQ/nixos.git
synced 2026-05-11 17:35:56 +08:00
347 lines
13 KiB
Python
347 lines
13 KiB
Python
# Generated by DeepSeek and Gemini
|
|
import argparse
|
|
import subprocess
|
|
import os
|
|
import shlex
|
|
import re # Import re for parsing and validation
|
|
|
|
|
|
# Function to get image dimensions (no changes needed here)
|
|
def get_image_dimensions(image_path):
|
|
"""Gets the width and height of an image using ImageMagick identify."""
|
|
# Append [0] to the image path to select the first frame,
|
|
# which works for both single-frame and multi-frame images.
|
|
image_path_with_frame = f"{image_path}[0]"
|
|
|
|
# Construct the command list correctly
|
|
cmd = ["magick", "identify", "-format", "%wx%h", image_path_with_frame]
|
|
# ^-- image_path_with_frame is now a single string element in the list
|
|
|
|
try:
|
|
process = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
output = process.stdout.strip()
|
|
if "x" in output:
|
|
# Handle potential output like '1920x1080+0+0' from some identify versions
|
|
# by splitting only on 'x' and taking the first two parts.
|
|
dims = output.split("x")
|
|
width = int(dims[0])
|
|
height = int(dims[1].split("+")[0]) # Handle potential offset info
|
|
return width, height
|
|
else:
|
|
print(
|
|
f"Warning: Could not parse dimensions from identify output: '{output}' for image '{image_path}'"
|
|
)
|
|
return None, None
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error running identify: {e}")
|
|
# Use cmd here, not the base command without the modified path
|
|
print(f"Command failed: {' '.join(shlex.quote(c) for c in cmd)}")
|
|
print("ImageMagick Error Output:\n", e.stderr)
|
|
return None, None
|
|
except FileNotFoundError:
|
|
print(
|
|
"Error: 'magick' command not found. Is ImageMagick installed and in your PATH?"
|
|
)
|
|
return None, None
|
|
except ValueError as e:
|
|
print(f"Error parsing dimensions '{output}' from identify output: {e}")
|
|
return None, None
|
|
except Exception as e:
|
|
# Catching the specific error from the traceback helps debugging
|
|
print(f"An unexpected error occurred during identify ({type(e).__name__}): {e}")
|
|
return None, None
|
|
|
|
|
|
# Function to calculate crop (no changes needed here)
|
|
def calculate_centered_crop(orig_w, orig_h, target_aspect_ratio):
|
|
"""Calculates the WxH for a centered crop preserving target aspect ratio."""
|
|
orig_aspect_ratio = orig_w / orig_h
|
|
|
|
if (
|
|
abs(orig_aspect_ratio - target_aspect_ratio) < 1e-6
|
|
): # Check for floating point equality
|
|
# Already the correct aspect ratio, no crop needed based on ratio
|
|
return f"{orig_w}x{orig_h}+0+0"
|
|
|
|
if orig_aspect_ratio > target_aspect_ratio:
|
|
# Original image is wider than target: crop width
|
|
crop_h = orig_h
|
|
crop_w = int(round(orig_h * target_aspect_ratio))
|
|
else:
|
|
# Original image is taller than target: crop height
|
|
crop_w = orig_w
|
|
crop_h = int(round(orig_w / target_aspect_ratio))
|
|
|
|
# Return geometry string suitable for -crop with -gravity Center
|
|
return f"{crop_w}x{crop_h}+0+0"
|
|
|
|
|
|
# Helper function to validate color format
|
|
def is_valid_color(color_string):
|
|
"""Checks if a string is 'blur' or a valid hex color (#RGB or #RRGGBB)."""
|
|
if color_string.lower() == "blur":
|
|
return True
|
|
# Simple hex color check (#RGB or #RRGGBB) - ImageMagick supports more formats,
|
|
# but this covers common web colors.
|
|
return re.fullmatch(r"#([0-9a-fA-F]{3}){1,2}", color_string) is not None
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Optionally crop an image, then overlay it on a background (blurred version or solid color) with a shadow."
|
|
)
|
|
parser.add_argument(
|
|
"-i", "--input", required=True, type=str, help="Input image file"
|
|
)
|
|
parser.add_argument(
|
|
"-o", "--output", type=str, default="output.jpg", help="Output image file"
|
|
)
|
|
|
|
# --- Cropping Arguments Group ---
|
|
crop_group = (
|
|
parser.add_mutually_exclusive_group()
|
|
) # Ensure only one crop method is used
|
|
crop_group.add_argument(
|
|
"-c",
|
|
"--crop",
|
|
type=str,
|
|
default=None,
|
|
help="Crop geometry BEFORE processing (e.g., '600x400+100+50'). See ImageMagick -crop documentation. Incompatible with --aspect-ratio-crop.",
|
|
)
|
|
crop_group.add_argument(
|
|
"--aspect-ratio-crop",
|
|
metavar="WxH", # Indicate expected format in help
|
|
type=str,
|
|
default=None,
|
|
help="Automatically crop to maintain the aspect ratio given by WxH (e.g., '16x9' or '2240x1400'), centering the crop. Incompatible with --crop.",
|
|
)
|
|
# --- End of Cropping Arguments ---
|
|
|
|
parser.add_argument(
|
|
"-s",
|
|
"--scale",
|
|
type=float,
|
|
default=0.8,
|
|
help="Scale factor for the overlay (e.g., 0.8 for 80%% of original size)",
|
|
)
|
|
# --- Background Argument ---
|
|
parser.add_argument(
|
|
"--background",
|
|
type=str,
|
|
default="blur",
|
|
help="Background type: 'blur' (default) for blurred input, or a hex color string (e.g., '#2a2a2a', '#FFF').",
|
|
)
|
|
# --- End Background Argument ---
|
|
parser.add_argument(
|
|
"--blur-arguments",
|
|
type=str,
|
|
default="0x20",
|
|
help="Arguments for the blur (e.g., '0x10' for sigma 10). Only used if --background is 'blur'.",
|
|
)
|
|
parser.add_argument(
|
|
"--shadow-arguments",
|
|
type=str,
|
|
default="80x40+0+0",
|
|
help="Arguments for the shadow (e.g., '80x3+0+0')",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not os.path.isfile(args.input):
|
|
print(f"Error: Input file '{args.input}' not found or is not a file.")
|
|
exit(1)
|
|
|
|
# --- Validate Background Argument ---
|
|
if not is_valid_color(args.background):
|
|
print(
|
|
f"Error: Invalid --background value '{args.background}'. "
|
|
"Must be 'blur' or a hex color like '#RRGGBB' or '#RGB'."
|
|
)
|
|
exit(1)
|
|
# --- End Validation ---
|
|
|
|
scale_percent = args.scale * 100
|
|
crop_geometry_to_use = None
|
|
use_gravity_center_for_crop = False
|
|
original_width, original_height = None, None # Store original dimensions if needed
|
|
|
|
# --- Determine Crop Geometry ---
|
|
if args.aspect_ratio_crop or args.crop or args.background != "blur":
|
|
# Get original dimensions if we need them for cropping or color background sizing
|
|
original_width, original_height = get_image_dimensions(args.input)
|
|
if original_width is None or original_height is None:
|
|
print("Error: Could not determine image dimensions.")
|
|
exit(1)
|
|
|
|
final_canvas_width = original_width
|
|
final_canvas_height = original_height
|
|
|
|
if args.aspect_ratio_crop:
|
|
# Validate and parse the aspect ratio string
|
|
match = re.fullmatch(r"(\d+)[x:](\d+)", args.aspect_ratio_crop)
|
|
if not match:
|
|
print(
|
|
f"Error: Invalid format for --aspect-ratio-crop '{args.aspect_ratio_crop}'. Use WxH or W:H (e.g., '16x9')."
|
|
)
|
|
exit(1)
|
|
try:
|
|
target_w = int(match.group(1))
|
|
target_h = int(match.group(2))
|
|
if target_w <= 0 or target_h <= 0:
|
|
raise ValueError("Width and height must be positive.")
|
|
target_aspect_ratio = target_w / target_h
|
|
except (ValueError, ZeroDivisionError) as e:
|
|
print(
|
|
f"Error: Invalid values in --aspect-ratio-crop '{args.aspect_ratio_crop}': {e}"
|
|
)
|
|
exit(1)
|
|
|
|
# Calculate the crop needed
|
|
crop_geometry_to_use = calculate_centered_crop(
|
|
original_width, original_height, target_aspect_ratio
|
|
)
|
|
use_gravity_center_for_crop = True # Center this type of crop
|
|
print(
|
|
f"Calculated aspect ratio crop geometry: {crop_geometry_to_use} (centered)"
|
|
)
|
|
# Update final canvas dimensions based on crop
|
|
# Extract WxH from geometry string 'WxH+X+Y'
|
|
crop_dims_match = re.match(r"(\d+)x(\d+)", crop_geometry_to_use)
|
|
if crop_dims_match:
|
|
final_canvas_width = int(crop_dims_match.group(1))
|
|
final_canvas_height = int(crop_dims_match.group(2))
|
|
else:
|
|
print(
|
|
f"Warning: Could not parse dimensions from calculated crop geometry '{crop_geometry_to_use}'"
|
|
)
|
|
# Fallback or error? For now, we'll proceed but background might be wrong size
|
|
final_canvas_width = original_width
|
|
final_canvas_height = original_height
|
|
|
|
elif args.crop:
|
|
# Use the manually specified crop geometry
|
|
crop_geometry_to_use = args.crop
|
|
use_gravity_center_for_crop = False # Do not force center for manual crop
|
|
print(f"Using manual crop geometry: {crop_geometry_to_use}")
|
|
# Update final canvas dimensions based on crop
|
|
crop_dims_match = re.match(r"(\d+)x(\d+)", crop_geometry_to_use)
|
|
if crop_dims_match:
|
|
final_canvas_width = int(crop_dims_match.group(1))
|
|
final_canvas_height = int(crop_dims_match.group(2))
|
|
else:
|
|
print(
|
|
f"Warning: Could not parse dimensions from manual crop geometry '{crop_geometry_to_use}'"
|
|
)
|
|
# Fallback or error?
|
|
final_canvas_width = original_width
|
|
final_canvas_height = original_height
|
|
|
|
# --- End Determine Crop Geometry ---
|
|
|
|
# --- Build the command list dynamically ---
|
|
cmd = ["magick"]
|
|
|
|
# 1. Add the input file
|
|
cmd.append(args.input)
|
|
|
|
# 2. Add cropping if specified
|
|
if crop_geometry_to_use:
|
|
if use_gravity_center_for_crop:
|
|
cmd.extend(["-gravity", "Center"])
|
|
cmd.extend(["-crop", crop_geometry_to_use])
|
|
cmd.append("+repage") # Reset page geometry after crop
|
|
print(f"Image will be cropped to {final_canvas_width}x{final_canvas_height}")
|
|
else:
|
|
print(f"Image dimensions: {final_canvas_width}x{final_canvas_height}")
|
|
|
|
# 3. Add the background layer generation
|
|
# This comes *after* the crop, operating on the cropped image dimensions.
|
|
# Stack: [cropped_image]
|
|
if args.background == "blur":
|
|
print(f"Using blurred background with arguments: {args.blur_arguments}")
|
|
cmd.extend(
|
|
[
|
|
"(",
|
|
"+clone", # Clone the (potentially cropped) image
|
|
"-blur",
|
|
args.blur_arguments,
|
|
")", # Result: [cropped_image, blurred_background]
|
|
]
|
|
)
|
|
else:
|
|
# Generate solid color background matching the (potentially cropped) image size
|
|
print(f"Using solid color background: {args.background}")
|
|
cmd.extend(
|
|
[
|
|
"(",
|
|
# Create a canvas of the correct size and color
|
|
"-size",
|
|
f"{final_canvas_width}x{final_canvas_height}",
|
|
f"canvas:{args.background}",
|
|
")", # Result: [cropped_image, color_background]
|
|
]
|
|
)
|
|
|
|
# 4. Add the overlay processing
|
|
# Stack is now: [cropped_image, background_layer]
|
|
cmd.extend(
|
|
[
|
|
"(",
|
|
"-clone",
|
|
"0", # Clone the original (cropped) image again (at index 0)
|
|
"-resize",
|
|
f"{scale_percent}%", # Resize this clone for the overlay
|
|
# Stack: [cropped_image, background_layer, resized_overlay]
|
|
"(",
|
|
"+clone", # Clone the resized overlay for shadow
|
|
"-background",
|
|
"black",
|
|
"-shadow",
|
|
args.shadow_arguments,
|
|
")", # Stack: [cropped_image, background_layer, resized_overlay, shadow]
|
|
"+swap", # Swap shadow and resized overlay
|
|
# Stack: [cropped_image, background_layer, shadow, resized_overlay]
|
|
"-background",
|
|
"none",
|
|
"-layers",
|
|
"merge", # Merge shadow onto resized overlay
|
|
"+repage", # Reset page geometry after merge
|
|
")", # Stack: [cropped_image, background_layer, overlay_with_shadow]
|
|
"-delete",
|
|
"0", # Delete the cropped_image at index 0
|
|
# Stack: [background_layer, overlay_with_shadow]
|
|
"-gravity",
|
|
"Center", # <<< Gravity for the FINAL composite
|
|
"-compose",
|
|
"over",
|
|
"-composite", # Composite overlay_with_shadow onto background_layer
|
|
args.output, # Specify the output file
|
|
]
|
|
)
|
|
# --- End of command building ---
|
|
|
|
print(f"Executing command: {' '.join(shlex.quote(c) for c in cmd)}")
|
|
|
|
try:
|
|
process = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
print(f"Successfully created {args.output}")
|
|
if process.stdout:
|
|
print("ImageMagick Output:\n", process.stdout)
|
|
if process.stderr:
|
|
print(
|
|
"ImageMagick Warnings/Messages:\n", process.stderr
|
|
) # Show non-error output too
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Error processing image: {e}")
|
|
print(f"Command failed: {' '.join(shlex.quote(c) for c in cmd)}")
|
|
print("ImageMagick Error Output:\n", e.stderr)
|
|
except FileNotFoundError:
|
|
print(
|
|
"Error: 'magick' command not found. Is ImageMagick installed and in your PATH?"
|
|
)
|
|
except Exception as e:
|
|
print(f"An unexpected error occurred ({type(e).__name__}): {e}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|