image_edit / basic_tools.py
mohamed12ahmed's picture
Upload 10 files
c3182bc verified
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)