import os from pathlib import Path from PIL import Image, ImageFilter, ImageOps, ImageDraw, ImageChops import tempfile def validate_path(p: str) -> Path: path = Path(os.path.expanduser(p)).resolve() if not path.exists(): raise FileNotFoundError(f"Path does not exist: {path}") return path def temp_output(suffix=".png"): return tempfile.NamedTemporaryFile(delete=False, suffix=suffix).name def ensure_path_from_img(img) -> Path: """ Ensures that the input is converted to a valid image file path. If `img` is a PIL.Image object, it saves it to a temporary file and returns the path. If `img` is already a path (string or Path), it wraps and returns it as a Path. Args: img: A PIL.Image or a path string Returns: Path to the image file """ if isinstance(img, Image.Image): temp_input = temp_output() img.save(temp_input) return Path(temp_input) return validate_path(img) def resize_image(input_path: str, width: int, height: int, output_path: str = None) -> str: """ Resize an image and save to a new file. Args: input_path (str): Path to source image. output_path (str): Path to save resized image. width (int): New width in pixels. height (int): New height in pixels. Returns: str: Path to resized image. """ if output_path is None: output_path = temp_output() input_path = validate_path(input_path) output_path = Path(output_path).expanduser().resolve() with Image.open(input_path) as img: img = img.resize((width, height)) output_path.parent.mkdir(parents=True, exist_ok=True) img.save(output_path) return str(output_path) def convert_grayscale(input_path: str, output_path: str = None) -> str: """ Convert an image to grayscale. Args: input_path (str): Source image path. output_path (str): Path to save grayscale version. Returns: str: Path to grayscale image. """ if output_path is None: output_path = temp_output() input_path = validate_path(input_path) output_path = Path(output_path).expanduser().resolve() with Image.open(input_path) as img: gray = img.convert("L") output_path.parent.mkdir(parents=True, exist_ok=True) gray.save(output_path) return str(output_path) def image_metadata(path: str) -> str: """ Get image metadata. Args: path (str): Image path. Returns: str: Format, size, and mode info. """ path = validate_path(path) with Image.open(path) as img: return "\n".join([ f"Format: {img.format}", f"Size: {img.size}", f"Mode: {img.mode}" ]) def convert_format(input_path: str, format: str, output_path: str = None) -> str: """ Convert an image to a new format. Args: input_path (str): Source image path. output_path (str): Destination path with extension. format (str): Format (e.g. 'png', 'jpeg'). Returns: str: Path to converted image. """ if output_path is None: output_path = temp_output() input_path = validate_path(input_path) output_path = Path(output_path).expanduser().resolve() with Image.open(input_path) as img: output_path.parent.mkdir(parents=True, exist_ok=True) img.save(output_path, format=format.upper()) return str(output_path) def blur_image(input_path: str, radius: float, output_path: str = None) -> str: """ Apply Gaussian blur to an image. Args: input_path (str): Image path. output_path (str): Path to save blurred image. radius (float): Blur radius. Returns: str: Path to blurred image. """ if output_path is None: output_path = temp_output() input_path = validate_path(input_path) output_path = Path(output_path).expanduser().resolve() with Image.open(input_path) as img: img = img.filter(ImageFilter.GaussianBlur(radius)) output_path.parent.mkdir(parents=True, exist_ok=True) img.save(output_path) return str(output_path) def rotate_image(input_path: str, angle: float, output_path: str = None) -> str: """ Rotate an image. Args: input_path (str): Source path. output_path (str): Destination path. angle (float): Rotation angle in degrees. Returns: str: Path to rotated image. """ if output_path is None: output_path = temp_output() input_path = validate_path(input_path) output_path = Path(output_path).expanduser().resolve() with Image.open(input_path) as img: img = img.rotate(angle, expand=True) output_path.parent.mkdir(parents=True, exist_ok=True) img.save(output_path) return str(output_path) def crop_image(input_path: str, left: int, top: int, right: int, bottom: int, output_path: str = None) -> str: """ Crop an image. Args: input_path (str): Source image. output_path (str): Destination. left, top, right, bottom (int): Crop box coordinates. Returns: str: Path to cropped image. """ if output_path is None: output_path = temp_output() input_path = validate_path(input_path) output_path = Path(output_path).expanduser().resolve() with Image.open(input_path) as img: img = img.crop((left, top, right, bottom)) output_path.parent.mkdir(parents=True, exist_ok=True) img.save(output_path) return str(output_path) def thumbnail_image(input_path: str, max_width: int, max_height: int, output_path: str = None) -> str: """ Resize image while maintaining aspect ratio. Args: input_path (str): Source. output_path (str): Target. max_width (int), max_height (int): Constraints. Returns: str: Path to thumbnail image. """ if output_path is None: output_path = temp_output() input_path = validate_path(input_path) output_path = Path(output_path).expanduser().resolve() with Image.open(input_path) as img: img.thumbnail((max_width, max_height)) output_path.parent.mkdir(parents=True, exist_ok=True) img.save(output_path) return str(output_path) def add_watermark(input_path: str, text: str, x: int, y: int, output_path: str = None) -> str: """ Add watermark text. Args: input_path (str): Image file. output_path (str): Save path. text (str): Watermark content. x (int), y (int): Position. Returns: str: Path to watermarked image. """ if output_path is None: output_path = temp_output() input_path = validate_path(input_path) output_path = Path(output_path).expanduser().resolve() with Image.open(input_path).convert("RGBA") as img: watermark = Image.new("RGBA", img.size) draw = ImageDraw.Draw(watermark) draw.text((x, y), text, fill=(255, 255, 255, 128)) img = Image.alpha_composite(img, watermark).convert("RGB") output_path.parent.mkdir(parents=True, exist_ok=True) img.save(output_path) return str(output_path) def flip_image(input_path: str, mode: str, output_path: str = None) -> str: """ Flip image horizontally or vertically. Args: input_path (str): Source. output_path (str): Destination. mode (str): 'horizontal' or 'vertical'. Returns: str: Path to flipped image. """ if output_path is None: output_path = temp_output() input_path = validate_path(input_path) output_path = Path(output_path).expanduser().resolve() with Image.open(input_path) as img: if mode == "horizontal": img = img.transpose(Image.FLIP_LEFT_RIGHT) elif mode == "vertical": img = img.transpose(Image.FLIP_TOP_BOTTOM) else: raise ValueError("Invalid mode") output_path.parent.mkdir(parents=True, exist_ok=True) img.save(output_path) return str(output_path) def invert_colors(input_path: str, output_path: str = None) -> str: """ Invert RGB color values. Args: input_path (str): Image file. output_path (str): Save location. Returns: str: Path to inverted image. """ if output_path is None: output_path = temp_output() input_path = validate_path(input_path) output_path = Path(output_path).expanduser().resolve() with Image.open(input_path) as img: img = ImageOps.invert(img.convert("RGB")) output_path.parent.mkdir(parents=True, exist_ok=True) img.save(output_path) return str(output_path) def list_images_in_directory(path: str) -> str: """ List image files in a directory. Args: path (str): Directory path. Returns: str: Newline-separated list of image paths. """ dir_path = Path(path).expanduser().resolve() if not dir_path.is_dir(): raise NotADirectoryError(f"{dir_path} is not a directory.") exts = {".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif"} files = [str(p) for p in dir_path.iterdir() if p.suffix.lower() in exts] return "\n".join(files) if files else "No image files found." def get_image_metadata(path: str) -> dict: """ Get metadata from an image. Args: path (str): Image path. Returns: dict: Metadata fields including format, size, mode. """ path = validate_path(path) with Image.open(path) as img: return { "path": str(path), "format": img.format, "mode": img.mode, "size": img.size, "width": img.width, "height": img.height, "info": img.info } import random def apply_random_color_variation(input_path: str, strength: float = 0.1,output_path: str = None) -> str: """ Apply random color variation to an image and save the result. Args: input_path (str): Source image path. output_path (str): Target image path. strength (float): Amount of variation per channel (0.0 to 1.0, default = 0.1). Returns: str: Path to the color-augmented image. """ if output_path is None: output_path = temp_output() input_path = validate_path(input_path) output_path = Path(output_path).expanduser().resolve() with Image.open(input_path).convert("RGB") as img: pixels = img.load() for y in range(img.height): for x in range(img.width): r, g, b = pixels[x, y] r = int(min(max(r + random.randint(-int(255 * strength), int(255 * strength)), 0), 255)) g = int(min(max(g + random.randint(-int(255 * strength), int(255 * strength)), 0), 255)) b = int(min(max(b + random.randint(-int(255 * strength), int(255 * strength)), 0), 255)) pixels[x, y] = (r, g, b) output_path.parent.mkdir(parents=True, exist_ok=True) img.save(output_path) return str(output_path) def add_images(image_path1: str, image_path2: str, output_path: str = None) -> str: """ Add two images pixel-wise and save the result. Args: image_path1 (str): Path to the first image. image_path2 (str): Path to the second image. output_path (str): Path to save the resulting image. Returns: str: Path to the added image. """ if output_path is None: output_path = temp_output() path1 = validate_path(image_path1) path2 = validate_path(image_path2) output_path = Path(output_path).expanduser().resolve() with Image.open(path1).convert("RGB") as img1, Image.open(path2).convert("RGB") as img2: if img1.size != img2.size: raise ValueError("Images must be the same size") result = ImageChops.add(img1, img2, scale=1.0, offset=0) output_path.parent.mkdir(parents=True, exist_ok=True) result.save(output_path) return str(output_path) from PIL import Image from pathlib import Path def concat_images(image_path1: str, image_path2: str, mode: str = "horizontal", output_path: str = None) -> str: """ Concatenate two images either horizontally or vertically. Args: image_path1 (str): Path to the first image. image_path2 (str): Path to the second image. output_path (str): Path to save the concatenated image. mode (str): 'horizontal' or 'vertical'. Returns: str: Path to the output image. """ if output_path is None: output_path = temp_output() img1 = Image.open(image_path1).convert("RGB") img2 = Image.open(image_path2).convert("RGB") if mode == "horizontal": new_width = img1.width + img2.width new_height = max(img1.height, img2.height) new_img = Image.new("RGB", (new_width, new_height)) new_img.paste(img1, (0, 0)) new_img.paste(img2, (img1.width, 0)) elif mode == "vertical": new_width = max(img1.width, img2.width) new_height = img1.height + img2.height new_img = Image.new("RGB", (new_width, new_height)) new_img.paste(img1, (0, 0)) new_img.paste(img2, (0, img1.height)) else: raise ValueError("Invalid mode. Use 'horizontal' or 'vertical'.") output_path = Path(output_path).expanduser().resolve() output_path.parent.mkdir(parents=True, exist_ok=True) new_img.save(output_path) return str(output_path)