mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-07-31 02:19:58 +03:00
[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
This commit is contained in:
parent
f33d985b40
commit
f6e9d8a307
|
@ -1,33 +1,51 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import json
|
||||
from PIL import Image, ImageFilter, ImageEnhance, ImageDraw, ImageFont, ImageOps
|
||||
from PIL.ExifTags import TAGS
|
||||
|
||||
import numpy as np
|
||||
|
||||
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont, ImageOps
|
||||
from PIL.ExifTags import TAGS
|
||||
|
||||
try:
|
||||
from PIL import ImageTk
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
from PIL import ImageTk
|
||||
|
||||
GUI_AVAILABLE = True
|
||||
except ImportError:
|
||||
GUI_AVAILABLE = False
|
||||
|
||||
|
||||
class PillowCLI:
|
||||
"""Main CLI class for image processing operations"""
|
||||
|
||||
def __init__(self):
|
||||
self.supported_formats = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp'}
|
||||
self.supported_formats = {
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".png",
|
||||
".bmp",
|
||||
".gif",
|
||||
".tiff",
|
||||
".webp",
|
||||
}
|
||||
|
||||
def validate_image_path(self, path):
|
||||
"""Validate if the path is a valid image file"""
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError(f"Image file not found: {path}")
|
||||
|
||||
if not Path(path).suffix.lower() in self.supported_formats:
|
||||
raise ValueError(f"Unsupported image format. Supported: {', '.join(self.supported_formats)}")
|
||||
if Path(path).suffix.lower() not in self.supported_formats:
|
||||
raise ValueError(
|
||||
f"Unsupported image format. Supported: {', '.join(self.supported_formats)}"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -39,18 +57,26 @@ class PillowCLI:
|
|||
def save_image(self, image, output_path, quality=95):
|
||||
"""Save image to file with specified quality"""
|
||||
# Ensure output directory exists
|
||||
os.makedirs(os.path.dirname(output_path) if os.path.dirname(output_path) else '.', exist_ok=True)
|
||||
os.makedirs(
|
||||
os.path.dirname(output_path) if os.path.dirname(output_path) else ".",
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
# Convert RGBA to RGB if saving as JPEG
|
||||
if Path(output_path).suffix.lower() in ['.jpg', '.jpeg'] and image.mode == 'RGBA':
|
||||
background = Image.new('RGB', image.size, (255, 255, 255))
|
||||
if (
|
||||
Path(output_path).suffix.lower() in [".jpg", ".jpeg"]
|
||||
and image.mode == "RGBA"
|
||||
):
|
||||
background = Image.new("RGB", image.size, (255, 255, 255))
|
||||
background.paste(image, mask=image.split()[-1])
|
||||
image = background
|
||||
|
||||
image.save(output_path, quality=quality, optimize=True)
|
||||
print(f"Image saved to: {output_path}")
|
||||
|
||||
def resize_image(self, input_path, output_path, width=None, height=None, maintain_aspect=True):
|
||||
def resize_image(
|
||||
self, input_path, output_path, width=None, height=None, maintain_aspect=True
|
||||
):
|
||||
"""Resize image with optional aspect ratio preservation"""
|
||||
image = self.load_image(input_path)
|
||||
original_width, original_height = image.size
|
||||
|
@ -75,35 +101,47 @@ class PillowCLI:
|
|||
|
||||
resized_image = image.resize(new_size, Image.Resampling.LANCZOS)
|
||||
self.save_image(resized_image, output_path)
|
||||
print(f"Resized from {original_width}x{original_height} to {new_size[0]}x{new_size[1]}")
|
||||
print(
|
||||
f"Resized from {original_width}x{original_height} to {new_size[0]}x{new_size[1]}"
|
||||
)
|
||||
|
||||
def apply_filters(self, input_path, output_path, filter_type):
|
||||
"""Apply various filters to the image"""
|
||||
image = self.load_image(input_path)
|
||||
|
||||
filters = {
|
||||
'blur': ImageFilter.BLUR,
|
||||
'contour': ImageFilter.CONTOUR,
|
||||
'detail': ImageFilter.DETAIL,
|
||||
'edge_enhance': ImageFilter.EDGE_ENHANCE,
|
||||
'edge_enhance_more': ImageFilter.EDGE_ENHANCE_MORE,
|
||||
'emboss': ImageFilter.EMBOSS,
|
||||
'find_edges': ImageFilter.FIND_EDGES,
|
||||
'sharpen': ImageFilter.SHARPEN,
|
||||
'smooth': ImageFilter.SMOOTH,
|
||||
'smooth_more': ImageFilter.SMOOTH_MORE,
|
||||
'gaussian_blur': ImageFilter.GaussianBlur(radius=2),
|
||||
'unsharp_mask': ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3)
|
||||
"blur": ImageFilter.BLUR,
|
||||
"contour": ImageFilter.CONTOUR,
|
||||
"detail": ImageFilter.DETAIL,
|
||||
"edge_enhance": ImageFilter.EDGE_ENHANCE,
|
||||
"edge_enhance_more": ImageFilter.EDGE_ENHANCE_MORE,
|
||||
"emboss": ImageFilter.EMBOSS,
|
||||
"find_edges": ImageFilter.FIND_EDGES,
|
||||
"sharpen": ImageFilter.SHARPEN,
|
||||
"smooth": ImageFilter.SMOOTH,
|
||||
"smooth_more": ImageFilter.SMOOTH_MORE,
|
||||
"gaussian_blur": ImageFilter.GaussianBlur(radius=2),
|
||||
"unsharp_mask": ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3),
|
||||
}
|
||||
|
||||
if filter_type not in filters:
|
||||
raise ValueError(f"Unknown filter: {filter_type}. Available: {', '.join(filters.keys())}")
|
||||
raise ValueError(
|
||||
f"Unknown filter: {filter_type}. Available: {', '.join(filters.keys())}"
|
||||
)
|
||||
|
||||
filtered_image = image.filter(filters[filter_type])
|
||||
self.save_image(filtered_image, output_path)
|
||||
print(f"Applied {filter_type} filter")
|
||||
|
||||
def adjust_image(self, input_path, output_path, brightness=1.0, contrast=1.0, saturation=1.0, sharpness=1.0):
|
||||
def adjust_image(
|
||||
self,
|
||||
input_path,
|
||||
output_path,
|
||||
brightness=1.0,
|
||||
contrast=1.0,
|
||||
saturation=1.0,
|
||||
sharpness=1.0,
|
||||
):
|
||||
"""Adjust image brightness, contrast, saturation, and sharpness"""
|
||||
image = self.load_image(input_path)
|
||||
|
||||
|
@ -125,7 +163,9 @@ class PillowCLI:
|
|||
image = enhancer.enhance(sharpness)
|
||||
|
||||
self.save_image(image, output_path)
|
||||
print(f"Adjusted image - Brightness: {brightness}, Contrast: {contrast}, Saturation: {saturation}, Sharpness: {sharpness}")
|
||||
print(
|
||||
f"Adjusted image - Brightness: {brightness}, Contrast: {contrast}, Saturation: {saturation}, Sharpness: {sharpness}"
|
||||
)
|
||||
|
||||
def crop_image(self, input_path, output_path, x, y, width, height):
|
||||
"""Crop image to specified dimensions"""
|
||||
|
@ -134,7 +174,9 @@ class PillowCLI:
|
|||
# Make sure we're not trying to crop outside the image bounds
|
||||
img_width, img_height = image.size
|
||||
if x + width > img_width or y + height > img_height:
|
||||
raise ValueError(f"Crop dimensions exceed image size ({img_width}x{img_height})")
|
||||
raise ValueError(
|
||||
f"Crop dimensions exceed image size ({img_width}x{img_height})"
|
||||
)
|
||||
|
||||
cropped_image = image.crop((x, y, x + width, y + height))
|
||||
self.save_image(cropped_image, output_path)
|
||||
|
@ -143,17 +185,17 @@ class PillowCLI:
|
|||
def rotate_image(self, input_path, output_path, angle, expand=True):
|
||||
"""Rotate image by specified angle"""
|
||||
image = self.load_image(input_path)
|
||||
rotated_image = image.rotate(angle, expand=expand, fillcolor='white')
|
||||
rotated_image = image.rotate(angle, expand=expand, fillcolor="white")
|
||||
self.save_image(rotated_image, output_path)
|
||||
print(f"Rotated image by {angle} degrees")
|
||||
|
||||
def flip_image(self, input_path, output_path, direction='horizontal'):
|
||||
def flip_image(self, input_path, output_path, direction="horizontal"):
|
||||
"""Flip image horizontally or vertically"""
|
||||
image = self.load_image(input_path)
|
||||
|
||||
if direction == 'horizontal':
|
||||
if direction == "horizontal":
|
||||
flipped_image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
|
||||
elif direction == 'vertical':
|
||||
elif direction == "vertical":
|
||||
flipped_image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
|
||||
else:
|
||||
raise ValueError("Direction must be 'horizontal' or 'vertical'")
|
||||
|
@ -161,14 +203,16 @@ class PillowCLI:
|
|||
self.save_image(flipped_image, output_path)
|
||||
print(f"Flipped image {direction}ly")
|
||||
|
||||
def convert_format(self, input_path, output_path, format_type='PNG'):
|
||||
def convert_format(self, input_path, output_path, format_type="PNG"):
|
||||
"""Convert image to different format"""
|
||||
image = self.load_image(input_path)
|
||||
|
||||
# Handle transparency for JPEG conversion
|
||||
if format_type.upper() == 'JPEG' and image.mode in ('RGBA', 'LA'):
|
||||
background = Image.new('RGB', image.size, (255, 255, 255))
|
||||
background.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
|
||||
if format_type.upper() == "JPEG" and image.mode in ("RGBA", "LA"):
|
||||
background = Image.new("RGB", image.size, (255, 255, 255))
|
||||
background.paste(
|
||||
image, mask=image.split()[-1] if image.mode == "RGBA" else None
|
||||
)
|
||||
image = background
|
||||
|
||||
self.save_image(image, output_path)
|
||||
|
@ -181,12 +225,19 @@ class PillowCLI:
|
|||
self.save_image(image, output_path)
|
||||
print(f"Created thumbnail with size {size}")
|
||||
|
||||
def add_watermark(self, input_path, output_path, watermark_text, position='bottom-right', opacity=128):
|
||||
def add_watermark(
|
||||
self,
|
||||
input_path,
|
||||
output_path,
|
||||
watermark_text,
|
||||
position="bottom-right",
|
||||
opacity=128,
|
||||
):
|
||||
"""Add text watermark to image"""
|
||||
image = self.load_image(input_path)
|
||||
|
||||
# Create a transparent overlay
|
||||
overlay = Image.new('RGBA', image.size, (255, 255, 255, 0))
|
||||
overlay = Image.new("RGBA", image.size, (255, 255, 255, 0))
|
||||
draw = ImageDraw.Draw(overlay)
|
||||
|
||||
# Try to use a default font, fallback to default if not available
|
||||
|
@ -203,24 +254,32 @@ class PillowCLI:
|
|||
# Calculate position
|
||||
img_width, img_height = image.size
|
||||
positions = {
|
||||
'top-left': (10, 10),
|
||||
'top-right': (img_width - text_width - 10, 10),
|
||||
'bottom-left': (10, img_height - text_height - 10),
|
||||
'bottom-right': (img_width - text_width - 10, img_height - text_height - 10),
|
||||
'center': ((img_width - text_width) // 2, (img_height - text_height) // 2)
|
||||
"top-left": (10, 10),
|
||||
"top-right": (img_width - text_width - 10, 10),
|
||||
"bottom-left": (10, img_height - text_height - 10),
|
||||
"bottom-right": (
|
||||
img_width - text_width - 10,
|
||||
img_height - text_height - 10,
|
||||
),
|
||||
"center": ((img_width - text_width) // 2, (img_height - text_height) // 2),
|
||||
}
|
||||
|
||||
if position not in positions:
|
||||
raise ValueError(f"Invalid position. Available: {', '.join(positions.keys())}")
|
||||
raise ValueError(
|
||||
f"Invalid position. Available: {', '.join(positions.keys())}"
|
||||
)
|
||||
|
||||
x, y = positions[position]
|
||||
|
||||
# Draw text with semi-transparent background
|
||||
draw.rectangle([x-5, y-5, x+text_width+5, y+text_height+5], fill=(0, 0, 0, opacity//2))
|
||||
draw.rectangle(
|
||||
[x - 5, y - 5, x + text_width + 5, y + text_height + 5],
|
||||
fill=(0, 0, 0, opacity // 2),
|
||||
)
|
||||
draw.text((x, y), watermark_text, font=font, fill=(255, 255, 255, opacity))
|
||||
|
||||
# Composite the overlay with the original image
|
||||
watermarked_image = Image.alpha_composite(image.convert('RGBA'), overlay)
|
||||
watermarked_image = Image.alpha_composite(image.convert("RGBA"), overlay)
|
||||
self.save_image(watermarked_image, output_path)
|
||||
print(f"Added watermark '{watermark_text}' at {position}")
|
||||
|
||||
|
@ -229,24 +288,24 @@ class PillowCLI:
|
|||
image = self.load_image(input_path)
|
||||
|
||||
metadata = {
|
||||
'filename': os.path.basename(input_path),
|
||||
'format': image.format,
|
||||
'mode': image.mode,
|
||||
'size': image.size,
|
||||
'width': image.width,
|
||||
'height': image.height,
|
||||
"filename": os.path.basename(input_path),
|
||||
"format": image.format,
|
||||
"mode": image.mode,
|
||||
"size": image.size,
|
||||
"width": image.width,
|
||||
"height": image.height,
|
||||
}
|
||||
|
||||
# Extract EXIF data if available
|
||||
exif_data = {}
|
||||
if hasattr(image, '_getexif') and image._getexif():
|
||||
if hasattr(image, "_getexif") and image._getexif():
|
||||
exif = image._getexif()
|
||||
for tag_id, value in exif.items():
|
||||
tag = TAGS.get(tag_id, tag_id)
|
||||
exif_data[tag] = value
|
||||
|
||||
if exif_data:
|
||||
metadata['exif'] = exif_data
|
||||
metadata["exif"] = exif_data
|
||||
|
||||
print(json.dumps(metadata, indent=2, default=str))
|
||||
return metadata
|
||||
|
@ -277,7 +336,7 @@ class PillowCLI:
|
|||
collage_height = rows * max_height + (rows + 1) * padding
|
||||
|
||||
# Create collage
|
||||
collage = Image.new('RGB', (collage_width, collage_height), 'white')
|
||||
collage = Image.new("RGB", (collage_width, collage_height), "white")
|
||||
|
||||
for i, img in enumerate(resized_images):
|
||||
row = i // cols
|
||||
|
@ -289,11 +348,11 @@ class PillowCLI:
|
|||
self.save_image(collage, output_path)
|
||||
print(f"Created collage with {len(images)} images ({rows}x{cols} grid)")
|
||||
|
||||
def apply_artistic_effects(self, input_path, output_path, effect='sepia'):
|
||||
def apply_artistic_effects(self, input_path, output_path, effect="sepia"):
|
||||
"""Apply artistic effects to the image"""
|
||||
image = self.load_image(input_path).convert('RGB')
|
||||
image = self.load_image(input_path).convert("RGB")
|
||||
|
||||
if effect == 'sepia':
|
||||
if effect == "sepia":
|
||||
# Convert to sepia
|
||||
pixels = image.load()
|
||||
for y in range(image.height):
|
||||
|
@ -304,20 +363,22 @@ class PillowCLI:
|
|||
tb = int(0.272 * r + 0.534 * g + 0.131 * b)
|
||||
pixels[x, y] = (min(255, tr), min(255, tg), min(255, tb))
|
||||
|
||||
elif effect == 'grayscale':
|
||||
elif effect == "grayscale":
|
||||
image = ImageOps.grayscale(image)
|
||||
|
||||
elif effect == 'invert':
|
||||
elif effect == "invert":
|
||||
image = ImageOps.invert(image)
|
||||
|
||||
elif effect == 'posterize':
|
||||
elif effect == "posterize":
|
||||
image = ImageOps.posterize(image, 4)
|
||||
|
||||
elif effect == 'solarize':
|
||||
elif effect == "solarize":
|
||||
image = ImageOps.solarize(image, threshold=128)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown effect: {effect}. Available: sepia, grayscale, invert, posterize, solarize")
|
||||
raise ValueError(
|
||||
f"Unknown effect: {effect}. Available: sepia, grayscale, invert, posterize, solarize"
|
||||
)
|
||||
|
||||
self.save_image(image, output_path)
|
||||
print(f"Applied {effect} effect")
|
||||
|
@ -349,15 +410,17 @@ class PillowCLI:
|
|||
output_file = output_path / image_file.name
|
||||
print(f"[{i}/{len(image_files)}] Processing: {image_file.name}")
|
||||
|
||||
if operation == 'resize':
|
||||
if operation == "resize":
|
||||
self.resize_image(str(image_file), str(output_file), **kwargs)
|
||||
elif operation == 'filter':
|
||||
elif operation == "filter":
|
||||
self.apply_filters(str(image_file), str(output_file), **kwargs)
|
||||
elif operation == 'adjust':
|
||||
elif operation == "adjust":
|
||||
self.adjust_image(str(image_file), str(output_file), **kwargs)
|
||||
elif operation == 'effect':
|
||||
self.apply_artistic_effects(str(image_file), str(output_file), **kwargs)
|
||||
elif operation == 'thumbnail':
|
||||
elif operation == "effect":
|
||||
self.apply_artistic_effects(
|
||||
str(image_file), str(output_file), **kwargs
|
||||
)
|
||||
elif operation == "thumbnail":
|
||||
self.create_thumbnail(str(image_file), str(output_file), **kwargs)
|
||||
else:
|
||||
print(f"Unknown batch operation: {operation}")
|
||||
|
@ -387,11 +450,11 @@ class PillowCLI:
|
|||
if operation == "Resize":
|
||||
self.resize_image(input_file, output_file, width=300, height=300)
|
||||
elif operation == "Blur":
|
||||
self.apply_filters(input_file, output_file, 'blur')
|
||||
self.apply_filters(input_file, output_file, "blur")
|
||||
elif operation == "Sepia":
|
||||
self.apply_artistic_effects(input_file, output_file, 'sepia')
|
||||
self.apply_artistic_effects(input_file, output_file, "sepia")
|
||||
elif operation == "Grayscale":
|
||||
self.apply_artistic_effects(input_file, output_file, 'grayscale')
|
||||
self.apply_artistic_effects(input_file, output_file, "grayscale")
|
||||
|
||||
status_var.set(f"Success! Processed image saved to {output_file}")
|
||||
except Exception as e:
|
||||
|
@ -415,8 +478,11 @@ class PillowCLI:
|
|||
ttk.Entry(root, textvariable=output_var, width=60).pack(pady=5)
|
||||
|
||||
ttk.Label(root, text="Operation:").pack(pady=5)
|
||||
ttk.Combobox(root, textvariable=operation_var,
|
||||
values=["Resize", "Blur", "Sepia", "Grayscale"]).pack(pady=5)
|
||||
ttk.Combobox(
|
||||
root,
|
||||
textvariable=operation_var,
|
||||
values=["Resize", "Blur", "Sepia", "Grayscale"],
|
||||
).pack(pady=5)
|
||||
|
||||
ttk.Button(root, text="Process Image", command=process_image).pack(pady=20)
|
||||
|
||||
|
@ -427,7 +493,7 @@ class PillowCLI:
|
|||
|
||||
def analyze_histogram(self, input_path, output_path=None):
|
||||
"""Analyze and optionally save image histogram"""
|
||||
image = self.load_image(input_path).convert('RGB')
|
||||
image = self.load_image(input_path).convert("RGB")
|
||||
|
||||
# Calculate histogram for each channel
|
||||
hist_r = image.histogram()[0:256]
|
||||
|
@ -435,10 +501,10 @@ class PillowCLI:
|
|||
hist_b = image.histogram()[512:768]
|
||||
|
||||
histogram_data = {
|
||||
'red_channel': hist_r,
|
||||
'green_channel': hist_g,
|
||||
'blue_channel': hist_b,
|
||||
'total_pixels': image.width * image.height
|
||||
"red_channel": hist_r,
|
||||
"green_channel": hist_g,
|
||||
"blue_channel": hist_b,
|
||||
"total_pixels": image.width * image.height,
|
||||
}
|
||||
|
||||
print(f"Image: {os.path.basename(input_path)}")
|
||||
|
@ -453,7 +519,7 @@ class PillowCLI:
|
|||
print(f"Average RGB values: R={avg_r:.1f}, G={avg_g:.1f}, B={avg_b:.1f}")
|
||||
|
||||
if output_path:
|
||||
with open(output_path, 'w') as f:
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(histogram_data, f, indent=2)
|
||||
print(f"Histogram data saved to: {output_path}")
|
||||
|
||||
|
@ -461,7 +527,7 @@ class PillowCLI:
|
|||
|
||||
def extract_color_palette(self, input_path, num_colors=5):
|
||||
"""Extract dominant colors from image"""
|
||||
image = self.load_image(input_path).convert('RGB')
|
||||
image = self.load_image(input_path).convert("RGB")
|
||||
|
||||
# Resize image for faster processing
|
||||
image = image.resize((150, 150))
|
||||
|
@ -473,13 +539,14 @@ class PillowCLI:
|
|||
# Use k-means clustering to find dominant colors
|
||||
try:
|
||||
from sklearn.cluster import KMeans
|
||||
|
||||
kmeans = KMeans(n_clusters=num_colors, random_state=42, n_init=10)
|
||||
kmeans.fit(pixels)
|
||||
colors = kmeans.cluster_centers_.astype(int)
|
||||
|
||||
print(f"Dominant colors in {os.path.basename(input_path)}:")
|
||||
for i, color in enumerate(colors, 1):
|
||||
hex_color = '#{:02x}{:02x}{:02x}'.format(color[0], color[1], color[2])
|
||||
hex_color = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
|
||||
print(f" {i}. RGB({color[0]}, {color[1]}, {color[2]}) - {hex_color}")
|
||||
|
||||
return colors.tolist()
|
||||
|
@ -492,33 +559,39 @@ class PillowCLI:
|
|||
unique_colors[color] = unique_colors.get(color, 0) + 1
|
||||
|
||||
# Get most common colors
|
||||
sorted_colors = sorted(unique_colors.items(), key=lambda x: x[1], reverse=True)
|
||||
sorted_colors = sorted(
|
||||
unique_colors.items(), key=lambda x: x[1], reverse=True
|
||||
)
|
||||
top_colors = [list(color[0]) for color in sorted_colors[:num_colors]]
|
||||
|
||||
print(f"Most common colors in {os.path.basename(input_path)}:")
|
||||
for i, color in enumerate(top_colors, 1):
|
||||
hex_color = '#{:02x}{:02x}{:02x}'.format(color[0], color[1], color[2])
|
||||
hex_color = f"#{color[0]:02x}{color[1]:02x}{color[2]:02x}"
|
||||
print(f" {i}. RGB({color[0]}, {color[1]}, {color[2]}) - {hex_color}")
|
||||
|
||||
return top_colors
|
||||
|
||||
def create_border(self, input_path, output_path, border_width=10, border_color='black'):
|
||||
def create_border(
|
||||
self, input_path, output_path, border_width=10, border_color="black"
|
||||
):
|
||||
"""Add border around image"""
|
||||
image = self.load_image(input_path)
|
||||
|
||||
# Parse border color
|
||||
if isinstance(border_color, str):
|
||||
if border_color.startswith('#'):
|
||||
if border_color.startswith("#"):
|
||||
# Hex color
|
||||
border_color = tuple(int(border_color[i:i+2], 16) for i in (1, 3, 5))
|
||||
elif border_color in ['black', 'white', 'red', 'green', 'blue']:
|
||||
border_color = tuple(
|
||||
int(border_color[i : i + 2], 16) for i in (1, 3, 5)
|
||||
)
|
||||
elif border_color in ["black", "white", "red", "green", "blue"]:
|
||||
# Named colors
|
||||
color_map = {
|
||||
'black': (0, 0, 0),
|
||||
'white': (255, 255, 255),
|
||||
'red': (255, 0, 0),
|
||||
'green': (0, 255, 0),
|
||||
'blue': (0, 0, 255)
|
||||
"black": (0, 0, 0),
|
||||
"white": (255, 255, 255),
|
||||
"red": (255, 0, 0),
|
||||
"green": (0, 255, 0),
|
||||
"blue": (0, 0, 255),
|
||||
}
|
||||
border_color = color_map[border_color]
|
||||
|
||||
|
@ -526,10 +599,12 @@ class PillowCLI:
|
|||
self.save_image(bordered_image, output_path)
|
||||
print(f"Added {border_width}px border with color {border_color}")
|
||||
|
||||
def create_composite(self, background_path, overlay_path, output_path, position=(0, 0), opacity=1.0):
|
||||
def create_composite(
|
||||
self, background_path, overlay_path, output_path, position=(0, 0), opacity=1.0
|
||||
):
|
||||
"""Composite two images together"""
|
||||
background = self.load_image(background_path).convert('RGBA')
|
||||
overlay = self.load_image(overlay_path).convert('RGBA')
|
||||
background = self.load_image(background_path).convert("RGBA")
|
||||
overlay = self.load_image(overlay_path).convert("RGBA")
|
||||
|
||||
# Adjust overlay opacity
|
||||
if opacity < 1.0:
|
||||
|
@ -537,7 +612,7 @@ class PillowCLI:
|
|||
overlay.putalpha(int(255 * opacity))
|
||||
|
||||
# Create composite
|
||||
composite = Image.new('RGBA', background.size, (0, 0, 0, 0))
|
||||
composite = Image.new("RGBA", background.size, (0, 0, 0, 0))
|
||||
composite.paste(background, (0, 0))
|
||||
composite.paste(overlay, position, overlay)
|
||||
|
||||
|
@ -546,11 +621,11 @@ class PillowCLI:
|
|||
|
||||
def apply_vignette(self, input_path, output_path, strength=0.5):
|
||||
"""Apply vignette effect to image"""
|
||||
image = self.load_image(input_path).convert('RGBA')
|
||||
image = self.load_image(input_path).convert("RGBA")
|
||||
width, height = image.size
|
||||
|
||||
# Create vignette mask
|
||||
mask = Image.new('L', (width, height), 0)
|
||||
mask = Image.new("L", (width, height), 0)
|
||||
mask_draw = ImageDraw.Draw(mask)
|
||||
|
||||
# Calculate center and radii
|
||||
|
@ -562,18 +637,27 @@ class PillowCLI:
|
|||
for i in range(steps):
|
||||
radius = max_radius * (i / steps)
|
||||
alpha = int(255 * (1 - strength * (i / steps)))
|
||||
mask_draw.ellipse([center_x - radius, center_y - radius,
|
||||
center_x + radius, center_y + radius], fill=alpha)
|
||||
mask_draw.ellipse(
|
||||
[
|
||||
center_x - radius,
|
||||
center_y - radius,
|
||||
center_x + radius,
|
||||
center_y + radius,
|
||||
],
|
||||
fill=alpha,
|
||||
)
|
||||
|
||||
# Apply vignette
|
||||
vignette_overlay = Image.new('RGBA', (width, height), (0, 0, 0, 0))
|
||||
vignette_overlay = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
||||
vignette_overlay.putalpha(mask)
|
||||
|
||||
result = Image.alpha_composite(image, vignette_overlay)
|
||||
self.save_image(result, output_path)
|
||||
print(f"Applied vignette effect with strength {strength}")
|
||||
|
||||
def create_contact_sheet(self, input_paths, output_path, sheet_width=1200, margin=10):
|
||||
def create_contact_sheet(
|
||||
self, input_paths, output_path, sheet_width=1200, margin=10
|
||||
):
|
||||
"""Create a contact sheet from multiple images"""
|
||||
if not input_paths:
|
||||
raise ValueError("No input images provided")
|
||||
|
@ -589,7 +673,7 @@ class PillowCLI:
|
|||
|
||||
# Create contact sheet
|
||||
sheet_height = rows * thumb_height + margin * (rows + 1)
|
||||
contact_sheet = Image.new('RGB', (sheet_width, sheet_height), 'white')
|
||||
contact_sheet = Image.new("RGB", (sheet_width, sheet_height), "white")
|
||||
|
||||
for i, img in enumerate(images):
|
||||
# Create thumbnail
|
||||
|
@ -614,7 +698,7 @@ class PillowCLI:
|
|||
|
||||
def split_channels(self, input_path, output_dir):
|
||||
"""Split image into separate color channels"""
|
||||
image = self.load_image(input_path).convert('RGB')
|
||||
image = self.load_image(input_path).convert("RGB")
|
||||
output_path = Path(output_dir)
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
@ -629,9 +713,15 @@ class PillowCLI:
|
|||
b_path = output_path / f"{base_name}_blue.png"
|
||||
|
||||
# Convert to RGB for saving (grayscale channels)
|
||||
r_img = Image.merge('RGB', [r, Image.new('L', image.size, 0), Image.new('L', image.size, 0)])
|
||||
g_img = Image.merge('RGB', [Image.new('L', image.size, 0), g, Image.new('L', image.size, 0)])
|
||||
b_img = Image.merge('RGB', [Image.new('L', image.size, 0), Image.new('L', image.size, 0), b])
|
||||
r_img = Image.merge(
|
||||
"RGB", [r, Image.new("L", image.size, 0), Image.new("L", image.size, 0)]
|
||||
)
|
||||
g_img = Image.merge(
|
||||
"RGB", [Image.new("L", image.size, 0), g, Image.new("L", image.size, 0)]
|
||||
)
|
||||
b_img = Image.merge(
|
||||
"RGB", [Image.new("L", image.size, 0), Image.new("L", image.size, 0), b]
|
||||
)
|
||||
|
||||
self.save_image(r_img, str(r_path))
|
||||
self.save_image(g_img, str(g_path))
|
||||
|
@ -640,113 +730,173 @@ class PillowCLI:
|
|||
print(f"Split channels saved to {output_dir}")
|
||||
return [str(r_path), str(g_path), str(b_path)]
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to handle command line arguments"""
|
||||
parser = argparse.ArgumentParser(description="Pillow CLI - Comprehensive Image Processing Tool")
|
||||
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Pillow CLI - Comprehensive Image Processing Tool"
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||
|
||||
# Resize command
|
||||
resize_parser = subparsers.add_parser('resize', help='Resize image')
|
||||
resize_parser.add_argument('input', help='Input image path')
|
||||
resize_parser.add_argument('output', help='Output image path')
|
||||
resize_parser.add_argument('--width', type=int, help='Target width')
|
||||
resize_parser.add_argument('--height', type=int, help='Target height')
|
||||
resize_parser.add_argument('--no-aspect', action='store_true', help='Don\'t maintain aspect ratio')
|
||||
resize_parser = subparsers.add_parser("resize", help="Resize image")
|
||||
resize_parser.add_argument("input", help="Input image path")
|
||||
resize_parser.add_argument("output", help="Output image path")
|
||||
resize_parser.add_argument("--width", type=int, help="Target width")
|
||||
resize_parser.add_argument("--height", type=int, help="Target height")
|
||||
resize_parser.add_argument(
|
||||
"--no-aspect", action="store_true", help="Don't maintain aspect ratio"
|
||||
)
|
||||
|
||||
# Filter command
|
||||
filter_parser = subparsers.add_parser('filter', help='Apply filter to image')
|
||||
filter_parser.add_argument('input', help='Input image path')
|
||||
filter_parser.add_argument('output', help='Output image path')
|
||||
filter_parser.add_argument('type', choices=['blur', 'contour', 'detail', 'edge_enhance',
|
||||
'edge_enhance_more', 'emboss', 'find_edges',
|
||||
'sharpen', 'smooth', 'smooth_more', 'gaussian_blur',
|
||||
'unsharp_mask'], help='Filter type')
|
||||
filter_parser = subparsers.add_parser("filter", help="Apply filter to image")
|
||||
filter_parser.add_argument("input", help="Input image path")
|
||||
filter_parser.add_argument("output", help="Output image path")
|
||||
filter_parser.add_argument(
|
||||
"type",
|
||||
choices=[
|
||||
"blur",
|
||||
"contour",
|
||||
"detail",
|
||||
"edge_enhance",
|
||||
"edge_enhance_more",
|
||||
"emboss",
|
||||
"find_edges",
|
||||
"sharpen",
|
||||
"smooth",
|
||||
"smooth_more",
|
||||
"gaussian_blur",
|
||||
"unsharp_mask",
|
||||
],
|
||||
help="Filter type",
|
||||
)
|
||||
|
||||
# Adjust command
|
||||
adjust_parser = subparsers.add_parser('adjust', help='Adjust image properties')
|
||||
adjust_parser.add_argument('input', help='Input image path')
|
||||
adjust_parser.add_argument('output', help='Output image path')
|
||||
adjust_parser.add_argument('--brightness', type=float, default=1.0, help='Brightness factor (default: 1.0)')
|
||||
adjust_parser.add_argument('--contrast', type=float, default=1.0, help='Contrast factor (default: 1.0)')
|
||||
adjust_parser.add_argument('--saturation', type=float, default=1.0, help='Saturation factor (default: 1.0)')
|
||||
adjust_parser.add_argument('--sharpness', type=float, default=1.0, help='Sharpness factor (default: 1.0)')
|
||||
adjust_parser = subparsers.add_parser("adjust", help="Adjust image properties")
|
||||
adjust_parser.add_argument("input", help="Input image path")
|
||||
adjust_parser.add_argument("output", help="Output image path")
|
||||
adjust_parser.add_argument(
|
||||
"--brightness", type=float, default=1.0, help="Brightness factor (default: 1.0)"
|
||||
)
|
||||
adjust_parser.add_argument(
|
||||
"--contrast", type=float, default=1.0, help="Contrast factor (default: 1.0)"
|
||||
)
|
||||
adjust_parser.add_argument(
|
||||
"--saturation", type=float, default=1.0, help="Saturation factor (default: 1.0)"
|
||||
)
|
||||
adjust_parser.add_argument(
|
||||
"--sharpness", type=float, default=1.0, help="Sharpness factor (default: 1.0)"
|
||||
)
|
||||
|
||||
# Crop command
|
||||
crop_parser = subparsers.add_parser('crop', help='Crop image')
|
||||
crop_parser.add_argument('input', help='Input image path')
|
||||
crop_parser.add_argument('output', help='Output image path')
|
||||
crop_parser.add_argument('x', type=int, help='X coordinate')
|
||||
crop_parser.add_argument('y', type=int, help='Y coordinate')
|
||||
crop_parser.add_argument('width', type=int, help='Crop width')
|
||||
crop_parser.add_argument('height', type=int, help='Crop height')
|
||||
crop_parser = subparsers.add_parser("crop", help="Crop image")
|
||||
crop_parser.add_argument("input", help="Input image path")
|
||||
crop_parser.add_argument("output", help="Output image path")
|
||||
crop_parser.add_argument("x", type=int, help="X coordinate")
|
||||
crop_parser.add_argument("y", type=int, help="Y coordinate")
|
||||
crop_parser.add_argument("width", type=int, help="Crop width")
|
||||
crop_parser.add_argument("height", type=int, help="Crop height")
|
||||
|
||||
# Rotate command
|
||||
rotate_parser = subparsers.add_parser('rotate', help='Rotate image')
|
||||
rotate_parser.add_argument('input', help='Input image path')
|
||||
rotate_parser.add_argument('output', help='Output image path')
|
||||
rotate_parser.add_argument('angle', type=float, help='Rotation angle in degrees')
|
||||
rotate_parser.add_argument('--no-expand', action='store_true', help='Don\'t expand canvas')
|
||||
rotate_parser = subparsers.add_parser("rotate", help="Rotate image")
|
||||
rotate_parser.add_argument("input", help="Input image path")
|
||||
rotate_parser.add_argument("output", help="Output image path")
|
||||
rotate_parser.add_argument("angle", type=float, help="Rotation angle in degrees")
|
||||
rotate_parser.add_argument(
|
||||
"--no-expand", action="store_true", help="Don't expand canvas"
|
||||
)
|
||||
|
||||
# Flip command
|
||||
flip_parser = subparsers.add_parser('flip', help='Flip image')
|
||||
flip_parser.add_argument('input', help='Input image path')
|
||||
flip_parser.add_argument('output', help='Output image path')
|
||||
flip_parser.add_argument('direction', choices=['horizontal', 'vertical'], help='Flip direction')
|
||||
flip_parser = subparsers.add_parser("flip", help="Flip image")
|
||||
flip_parser.add_argument("input", help="Input image path")
|
||||
flip_parser.add_argument("output", help="Output image path")
|
||||
flip_parser.add_argument(
|
||||
"direction", choices=["horizontal", "vertical"], help="Flip direction"
|
||||
)
|
||||
|
||||
# Convert command
|
||||
convert_parser = subparsers.add_parser('convert', help='Convert image format')
|
||||
convert_parser.add_argument('input', help='Input image path')
|
||||
convert_parser.add_argument('output', help='Output image path')
|
||||
convert_parser.add_argument('--format', default='PNG', help='Target format (default: PNG)')
|
||||
convert_parser = subparsers.add_parser("convert", help="Convert image format")
|
||||
convert_parser.add_argument("input", help="Input image path")
|
||||
convert_parser.add_argument("output", help="Output image path")
|
||||
convert_parser.add_argument(
|
||||
"--format", default="PNG", help="Target format (default: PNG)"
|
||||
)
|
||||
|
||||
# Thumbnail command
|
||||
thumbnail_parser = subparsers.add_parser('thumbnail', help='Create thumbnail')
|
||||
thumbnail_parser.add_argument('input', help='Input image path')
|
||||
thumbnail_parser.add_argument('output', help='Output image path')
|
||||
thumbnail_parser.add_argument('--size', type=int, nargs=2, default=[128, 128], help='Thumbnail size (width height)')
|
||||
thumbnail_parser = subparsers.add_parser("thumbnail", help="Create thumbnail")
|
||||
thumbnail_parser.add_argument("input", help="Input image path")
|
||||
thumbnail_parser.add_argument("output", help="Output image path")
|
||||
thumbnail_parser.add_argument(
|
||||
"--size",
|
||||
type=int,
|
||||
nargs=2,
|
||||
default=[128, 128],
|
||||
help="Thumbnail size (width height)",
|
||||
)
|
||||
|
||||
# Watermark command
|
||||
watermark_parser = subparsers.add_parser('watermark', help='Add watermark')
|
||||
watermark_parser.add_argument('input', help='Input image path')
|
||||
watermark_parser.add_argument('output', help='Output image path')
|
||||
watermark_parser.add_argument('text', help='Watermark text')
|
||||
watermark_parser.add_argument('--position', choices=['top-left', 'top-right', 'bottom-left',
|
||||
'bottom-right', 'center'],
|
||||
default='bottom-right', help='Watermark position')
|
||||
watermark_parser.add_argument('--opacity', type=int, default=128, help='Watermark opacity (0-255)')
|
||||
watermark_parser = subparsers.add_parser("watermark", help="Add watermark")
|
||||
watermark_parser.add_argument("input", help="Input image path")
|
||||
watermark_parser.add_argument("output", help="Output image path")
|
||||
watermark_parser.add_argument("text", help="Watermark text")
|
||||
watermark_parser.add_argument(
|
||||
"--position",
|
||||
choices=["top-left", "top-right", "bottom-left", "bottom-right", "center"],
|
||||
default="bottom-right",
|
||||
help="Watermark position",
|
||||
)
|
||||
watermark_parser.add_argument(
|
||||
"--opacity", type=int, default=128, help="Watermark opacity (0-255)"
|
||||
)
|
||||
|
||||
# Metadata command
|
||||
metadata_parser = subparsers.add_parser('metadata', help='Extract image metadata')
|
||||
metadata_parser.add_argument('input', help='Input image path')
|
||||
metadata_parser = subparsers.add_parser("metadata", help="Extract image metadata")
|
||||
metadata_parser.add_argument("input", help="Input image path")
|
||||
|
||||
# Collage command
|
||||
collage_parser = subparsers.add_parser('collage', help='Create collage from multiple images')
|
||||
collage_parser.add_argument('output', help='Output image path')
|
||||
collage_parser.add_argument('inputs', nargs='+', help='Input image paths')
|
||||
collage_parser.add_argument('--cols', type=int, default=2, help='Number of columns')
|
||||
collage_parser.add_argument('--padding', type=int, default=10, help='Padding between images')
|
||||
collage_parser = subparsers.add_parser(
|
||||
"collage", help="Create collage from multiple images"
|
||||
)
|
||||
collage_parser.add_argument("output", help="Output image path")
|
||||
collage_parser.add_argument("inputs", nargs="+", help="Input image paths")
|
||||
collage_parser.add_argument("--cols", type=int, default=2, help="Number of columns")
|
||||
collage_parser.add_argument(
|
||||
"--padding", type=int, default=10, help="Padding between images"
|
||||
)
|
||||
|
||||
# Effect command
|
||||
effect_parser = subparsers.add_parser('effect', help='Apply artistic effects')
|
||||
effect_parser.add_argument('input', help='Input image path')
|
||||
effect_parser.add_argument('output', help='Output image path')
|
||||
effect_parser.add_argument('type', choices=['sepia', 'grayscale', 'invert', 'posterize', 'solarize'],
|
||||
help='Effect type')
|
||||
effect_parser = subparsers.add_parser("effect", help="Apply artistic effects")
|
||||
effect_parser.add_argument("input", help="Input image path")
|
||||
effect_parser.add_argument("output", help="Output image path")
|
||||
effect_parser.add_argument(
|
||||
"type",
|
||||
choices=["sepia", "grayscale", "invert", "posterize", "solarize"],
|
||||
help="Effect type",
|
||||
)
|
||||
|
||||
# Batch command
|
||||
batch_parser = subparsers.add_parser('batch', help='Batch process images')
|
||||
batch_parser.add_argument('input_dir', help='Input directory')
|
||||
batch_parser.add_argument('output_dir', help='Output directory')
|
||||
batch_parser.add_argument('operation', choices=['resize', 'filter', 'adjust', 'effect', 'thumbnail'],
|
||||
help='Batch operation')
|
||||
batch_parser.add_argument('--width', type=int, help='Width for resize operation')
|
||||
batch_parser.add_argument('--height', type=int, help='Height for resize operation')
|
||||
batch_parser.add_argument('--filter-type', help='Filter type for filter operation')
|
||||
batch_parser.add_argument('--effect-type', help='Effect type for effect operation')
|
||||
batch_parser.add_argument('--size', type=int, nargs=2, default=[128, 128], help='Size for thumbnail operation')
|
||||
batch_parser = subparsers.add_parser("batch", help="Batch process images")
|
||||
batch_parser.add_argument("input_dir", help="Input directory")
|
||||
batch_parser.add_argument("output_dir", help="Output directory")
|
||||
batch_parser.add_argument(
|
||||
"operation",
|
||||
choices=["resize", "filter", "adjust", "effect", "thumbnail"],
|
||||
help="Batch operation",
|
||||
)
|
||||
batch_parser.add_argument("--width", type=int, help="Width for resize operation")
|
||||
batch_parser.add_argument("--height", type=int, help="Height for resize operation")
|
||||
batch_parser.add_argument("--filter-type", help="Filter type for filter operation")
|
||||
batch_parser.add_argument("--effect-type", help="Effect type for effect operation")
|
||||
batch_parser.add_argument(
|
||||
"--size",
|
||||
type=int,
|
||||
nargs=2,
|
||||
default=[128, 128],
|
||||
help="Size for thumbnail operation",
|
||||
)
|
||||
|
||||
# GUI command
|
||||
gui_parser = subparsers.add_parser('gui', help='Launch simple GUI')
|
||||
gui_parser = subparsers.add_parser("gui", help="Launch simple GUI")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
@ -757,63 +907,81 @@ def main():
|
|||
cli = PillowCLI()
|
||||
|
||||
try:
|
||||
if args.command == 'resize':
|
||||
cli.resize_image(args.input, args.output, args.width, args.height,
|
||||
maintain_aspect=not args.no_aspect)
|
||||
if args.command == "resize":
|
||||
cli.resize_image(
|
||||
args.input,
|
||||
args.output,
|
||||
args.width,
|
||||
args.height,
|
||||
maintain_aspect=not args.no_aspect,
|
||||
)
|
||||
|
||||
elif args.command == 'filter':
|
||||
elif args.command == "filter":
|
||||
cli.apply_filters(args.input, args.output, args.type)
|
||||
|
||||
elif args.command == 'adjust':
|
||||
cli.adjust_image(args.input, args.output, args.brightness, args.contrast,
|
||||
args.saturation, args.sharpness)
|
||||
elif args.command == "adjust":
|
||||
cli.adjust_image(
|
||||
args.input,
|
||||
args.output,
|
||||
args.brightness,
|
||||
args.contrast,
|
||||
args.saturation,
|
||||
args.sharpness,
|
||||
)
|
||||
|
||||
elif args.command == 'crop':
|
||||
cli.crop_image(args.input, args.output, args.x, args.y, args.width, args.height)
|
||||
elif args.command == "crop":
|
||||
cli.crop_image(
|
||||
args.input, args.output, args.x, args.y, args.width, args.height
|
||||
)
|
||||
|
||||
elif args.command == 'rotate':
|
||||
cli.rotate_image(args.input, args.output, args.angle, expand=not args.no_expand)
|
||||
elif args.command == "rotate":
|
||||
cli.rotate_image(
|
||||
args.input, args.output, args.angle, expand=not args.no_expand
|
||||
)
|
||||
|
||||
elif args.command == 'flip':
|
||||
elif args.command == "flip":
|
||||
cli.flip_image(args.input, args.output, args.direction)
|
||||
|
||||
elif args.command == 'convert':
|
||||
elif args.command == "convert":
|
||||
cli.convert_format(args.input, args.output, args.format)
|
||||
|
||||
elif args.command == 'thumbnail':
|
||||
elif args.command == "thumbnail":
|
||||
cli.create_thumbnail(args.input, args.output, tuple(args.size))
|
||||
|
||||
elif args.command == 'watermark':
|
||||
cli.add_watermark(args.input, args.output, args.text, args.position, args.opacity)
|
||||
elif args.command == "watermark":
|
||||
cli.add_watermark(
|
||||
args.input, args.output, args.text, args.position, args.opacity
|
||||
)
|
||||
|
||||
elif args.command == 'metadata':
|
||||
elif args.command == "metadata":
|
||||
cli.extract_metadata(args.input)
|
||||
|
||||
elif args.command == 'collage':
|
||||
elif args.command == "collage":
|
||||
cli.create_collage(args.inputs, args.output, args.cols, args.padding)
|
||||
|
||||
elif args.command == 'effect':
|
||||
elif args.command == "effect":
|
||||
cli.apply_artistic_effects(args.input, args.output, args.type)
|
||||
|
||||
elif args.command == 'batch':
|
||||
elif args.command == "batch":
|
||||
kwargs = {}
|
||||
if args.operation == 'resize':
|
||||
kwargs = {'width': args.width, 'height': args.height}
|
||||
elif args.operation == 'filter':
|
||||
kwargs = {'filter_type': args.filter_type}
|
||||
elif args.operation == 'effect':
|
||||
kwargs = {'effect': args.effect_type}
|
||||
elif args.operation == 'thumbnail':
|
||||
kwargs = {'size': tuple(args.size)}
|
||||
if args.operation == "resize":
|
||||
kwargs = {"width": args.width, "height": args.height}
|
||||
elif args.operation == "filter":
|
||||
kwargs = {"filter_type": args.filter_type}
|
||||
elif args.operation == "effect":
|
||||
kwargs = {"effect": args.effect_type}
|
||||
elif args.operation == "thumbnail":
|
||||
kwargs = {"size": tuple(args.size)}
|
||||
|
||||
cli.batch_process(args.input_dir, args.output_dir, args.operation, **kwargs)
|
||||
|
||||
elif args.command == 'gui':
|
||||
elif args.command == "gui":
|
||||
cli.simple_gui()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -2,10 +2,14 @@
|
|||
Test setup and fixtures for the Pillow CLI project
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import shutil
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
|
@ -20,7 +24,10 @@ def test_data_dir():
|
|||
@pytest.fixture
|
||||
def create_test_image():
|
||||
"""Helper to make test images on demand"""
|
||||
def _create_image(size=(100, 100), color='red', format='JPEG', mode='RGB', save_path=None):
|
||||
|
||||
def _create_image(
|
||||
size=(100, 100), color="red", format="JPEG", mode="RGB", save_path=None
|
||||
):
|
||||
image = Image.new(mode, size, color=color)
|
||||
|
||||
# Add some visual elements to make it more realistic
|
||||
|
@ -31,16 +38,19 @@ def create_test_image():
|
|||
x1, y1 = size[0] // 4, size[1] // 4
|
||||
x2, y2 = x1 + rect_size, y1 + rect_size
|
||||
|
||||
if mode == 'RGB':
|
||||
draw.rectangle([x1, y1, x2, y2], fill='blue')
|
||||
elif mode == 'RGBA':
|
||||
if mode == "RGB":
|
||||
draw.rectangle([x1, y1, x2, y2], fill="blue")
|
||||
elif mode == "RGBA":
|
||||
draw.rectangle([x1, y1, x2, y2], fill=(0, 0, 255, 200))
|
||||
|
||||
# Circle around the edge if there's room
|
||||
if size[0] >= 20 and size[1] >= 20:
|
||||
margin = 5
|
||||
draw.ellipse([margin, margin, size[0] - margin, size[1] - margin],
|
||||
outline='green', width=2)
|
||||
draw.ellipse(
|
||||
[margin, margin, size[0] - margin, size[1] - margin],
|
||||
outline="green",
|
||||
width=2,
|
||||
)
|
||||
|
||||
if save_path:
|
||||
image.save(save_path, format)
|
||||
|
@ -54,7 +64,7 @@ def create_test_image():
|
|||
def sample_images_batch(test_data_dir, create_test_image):
|
||||
"""Makes a batch of sample images for testing"""
|
||||
images = []
|
||||
colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange']
|
||||
colors = ["red", "green", "blue", "yellow", "purple", "orange"]
|
||||
sizes = [(50, 50), (100, 100), (150, 100), (100, 150)]
|
||||
|
||||
for i, (color, size) in enumerate(zip(colors, sizes * 2)): # cycle through sizes
|
||||
|
@ -74,34 +84,30 @@ def sample_formats(test_data_dir, create_test_image):
|
|||
|
||||
# JPEG
|
||||
jpeg_path = os.path.join(test_data_dir, "sample.jpg")
|
||||
formats_data['jpeg'] = create_test_image(save_path=jpeg_path, format='JPEG')
|
||||
formats_data["jpeg"] = create_test_image(save_path=jpeg_path, format="JPEG")
|
||||
|
||||
# PNG with transparency
|
||||
png_path = os.path.join(test_data_dir, "sample.png")
|
||||
formats_data['png'] = create_test_image(save_path=png_path, format='PNG', mode='RGBA')
|
||||
formats_data["png"] = create_test_image(
|
||||
save_path=png_path, format="PNG", mode="RGBA"
|
||||
)
|
||||
|
||||
# BMP
|
||||
bmp_path = os.path.join(test_data_dir, "sample.bmp")
|
||||
formats_data['bmp'] = create_test_image(save_path=bmp_path, format='BMP')
|
||||
formats_data["bmp"] = create_test_image(save_path=bmp_path, format="BMP")
|
||||
|
||||
# GIF
|
||||
gif_path = os.path.join(test_data_dir, "sample.gif")
|
||||
formats_data['gif'] = create_test_image(save_path=gif_path, format='GIF')
|
||||
formats_data["gif"] = create_test_image(save_path=gif_path, format="GIF")
|
||||
|
||||
return formats_data
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Add our custom test markers"""
|
||||
config.addinivalue_line(
|
||||
"markers", "unit: mark test as a unit test"
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers", "integration: mark test as an integration test"
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers", "slow: mark test as slow running"
|
||||
)
|
||||
config.addinivalue_line("markers", "unit: mark test as a unit test")
|
||||
config.addinivalue_line("markers", "integration: mark test as an integration test")
|
||||
config.addinivalue_line("markers", "slow: mark test as slow running")
|
||||
config.addinivalue_line(
|
||||
"markers", "gui: mark test as GUI-related (may require display)"
|
||||
)
|
||||
|
|
|
@ -3,20 +3,22 @@
|
|||
Test suite for Pillow CLI tool
|
||||
Comprehensive tests covering all functionality
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import numpy as np
|
||||
from unittest.mock import patch, MagicMock, mock_open
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# Import the CLI class
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
from pillow_cli import PillowCLI
|
||||
|
||||
|
@ -40,6 +42,7 @@ class TestPillowCLI:
|
|||
except PermissionError:
|
||||
# Try again after a brief delay
|
||||
import time
|
||||
|
||||
time.sleep(0.1)
|
||||
try:
|
||||
shutil.rmtree(temp_dir)
|
||||
|
@ -51,33 +54,33 @@ class TestPillowCLI:
|
|||
"""Create a sample image for testing"""
|
||||
image_path = os.path.join(temp_dir, "sample.jpg")
|
||||
# Create a simple 100x100 RGB image
|
||||
image = Image.new('RGB', (100, 100), color='red')
|
||||
image = Image.new("RGB", (100, 100), color="red")
|
||||
# Add some content to make it more realistic
|
||||
draw = ImageDraw.Draw(image)
|
||||
draw.rectangle([25, 25, 75, 75], fill='blue')
|
||||
draw.ellipse([10, 10, 90, 90], outline='green', width=3)
|
||||
image.save(image_path, 'JPEG')
|
||||
draw.rectangle([25, 25, 75, 75], fill="blue")
|
||||
draw.ellipse([10, 10, 90, 90], outline="green", width=3)
|
||||
image.save(image_path, "JPEG")
|
||||
return image_path
|
||||
|
||||
@pytest.fixture
|
||||
def sample_png_image(self, temp_dir):
|
||||
"""Create a sample PNG image with transparency"""
|
||||
image_path = os.path.join(temp_dir, "sample.png")
|
||||
image = Image.new('RGBA', (100, 100), color=(255, 0, 0, 128))
|
||||
image = Image.new("RGBA", (100, 100), color=(255, 0, 0, 128))
|
||||
draw = ImageDraw.Draw(image)
|
||||
draw.rectangle([25, 25, 75, 75], fill=(0, 0, 255, 200))
|
||||
image.save(image_path, 'PNG')
|
||||
image.save(image_path, "PNG")
|
||||
return image_path
|
||||
|
||||
@pytest.fixture
|
||||
def multiple_sample_images(self, temp_dir):
|
||||
"""Create multiple sample images for collage testing"""
|
||||
images = []
|
||||
colors = ['red', 'green', 'blue', 'yellow']
|
||||
colors = ["red", "green", "blue", "yellow"]
|
||||
for i, color in enumerate(colors):
|
||||
image_path = os.path.join(temp_dir, f"sample_{i}.jpg")
|
||||
image = Image.new('RGB', (50, 50), color=color)
|
||||
image.save(image_path, 'JPEG')
|
||||
image = Image.new("RGB", (50, 50), color=color)
|
||||
image.save(image_path, "JPEG")
|
||||
images.append(image_path)
|
||||
return images
|
||||
|
||||
|
@ -85,8 +88,8 @@ class TestPillowCLI:
|
|||
def test_cli_initialization(self, cli):
|
||||
"""Test CLI initialization"""
|
||||
assert isinstance(cli, PillowCLI)
|
||||
assert hasattr(cli, 'supported_formats')
|
||||
expected_formats = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp'}
|
||||
assert hasattr(cli, "supported_formats")
|
||||
expected_formats = {".jpg", ".jpeg", ".png", ".bmp", ".gif", ".tiff", ".webp"}
|
||||
assert cli.supported_formats == expected_formats
|
||||
|
||||
# Test image validation
|
||||
|
@ -102,7 +105,7 @@ class TestPillowCLI:
|
|||
def test_validate_image_path_unsupported_format(self, cli, temp_dir):
|
||||
"""Test validation of unsupported image format"""
|
||||
unsupported_file = os.path.join(temp_dir, "test.txt")
|
||||
with open(unsupported_file, 'w') as f:
|
||||
with open(unsupported_file, "w") as f:
|
||||
f.write("test")
|
||||
|
||||
with pytest.raises(ValueError, match="Unsupported image format"):
|
||||
|
@ -114,15 +117,15 @@ class TestPillowCLI:
|
|||
image = cli.load_image(sample_image)
|
||||
assert isinstance(image, Image.Image)
|
||||
assert image.size == (100, 100)
|
||||
assert image.mode in ['RGB', 'RGBA']
|
||||
assert image.mode in ["RGB", "RGBA"]
|
||||
|
||||
# Test image saving
|
||||
def test_save_image_jpg(self, cli, temp_dir):
|
||||
"""Test saving image as JPEG"""
|
||||
image = Image.new('RGB', (50, 50), color='red')
|
||||
image = Image.new("RGB", (50, 50), color="red")
|
||||
output_path = os.path.join(temp_dir, "output.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.save_image(image, output_path)
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
|
@ -132,18 +135,18 @@ class TestPillowCLI:
|
|||
|
||||
def test_save_image_rgba_to_jpg(self, cli, temp_dir):
|
||||
"""Test saving RGBA image as JPEG (should convert to RGB)"""
|
||||
image = Image.new('RGBA', (50, 50), color=(255, 0, 0, 128))
|
||||
image = Image.new("RGBA", (50, 50), color=(255, 0, 0, 128))
|
||||
output_path = os.path.join(temp_dir, "output.jpg")
|
||||
|
||||
cli.save_image(image, output_path)
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
saved_image = Image.open(output_path)
|
||||
assert saved_image.mode == 'RGB'
|
||||
assert saved_image.mode == "RGB"
|
||||
|
||||
def test_save_image_create_directory(self, cli, temp_dir):
|
||||
"""Test saving image creates output directory if it doesn't exist"""
|
||||
image = Image.new('RGB', (50, 50), color='red')
|
||||
image = Image.new("RGB", (50, 50), color="red")
|
||||
output_path = os.path.join(temp_dir, "subdir", "output.jpg")
|
||||
|
||||
cli.save_image(image, output_path)
|
||||
|
@ -155,7 +158,7 @@ class TestPillowCLI:
|
|||
"""Test resizing with both width and height specified"""
|
||||
output_path = os.path.join(temp_dir, "resized.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.resize_image(sample_image, output_path, width=200, height=150)
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
|
@ -190,11 +193,15 @@ class TestPillowCLI:
|
|||
with pytest.raises(ValueError, match="Must specify at least width or height"):
|
||||
cli.resize_image(sample_image, output_path)
|
||||
|
||||
def test_resize_image_no_aspect_ratio_maintenance(self, cli, sample_image, temp_dir):
|
||||
def test_resize_image_no_aspect_ratio_maintenance(
|
||||
self, cli, sample_image, temp_dir
|
||||
):
|
||||
"""Test resizing without maintaining aspect ratio"""
|
||||
output_path = os.path.join(temp_dir, "resized.jpg")
|
||||
|
||||
cli.resize_image(sample_image, output_path, width=200, height=100, maintain_aspect=False)
|
||||
cli.resize_image(
|
||||
sample_image, output_path, width=200, height=100, maintain_aspect=False
|
||||
)
|
||||
|
||||
resized_image = Image.open(output_path)
|
||||
assert resized_image.size == (200, 100)
|
||||
|
@ -204,16 +211,24 @@ class TestPillowCLI:
|
|||
"""Test applying blur filter"""
|
||||
output_path = os.path.join(temp_dir, "blurred.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
cli.apply_filters(sample_image, output_path, 'blur')
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.apply_filters(sample_image, output_path, "blur")
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
mock_print.assert_any_call("Applied blur filter")
|
||||
|
||||
def test_apply_filters_all_types(self, cli, sample_image, temp_dir):
|
||||
"""Test all available filter types"""
|
||||
filter_types = ['blur', 'contour', 'detail', 'edge_enhance', 'sharpen',
|
||||
'smooth', 'gaussian_blur', 'unsharp_mask']
|
||||
filter_types = [
|
||||
"blur",
|
||||
"contour",
|
||||
"detail",
|
||||
"edge_enhance",
|
||||
"sharpen",
|
||||
"smooth",
|
||||
"gaussian_blur",
|
||||
"unsharp_mask",
|
||||
]
|
||||
|
||||
for filter_type in filter_types:
|
||||
output_path = os.path.join(temp_dir, f"{filter_type}.jpg")
|
||||
|
@ -225,20 +240,27 @@ class TestPillowCLI:
|
|||
output_path = os.path.join(temp_dir, "filtered.jpg")
|
||||
|
||||
with pytest.raises(ValueError, match="Unknown filter"):
|
||||
cli.apply_filters(sample_image, output_path, 'invalid_filter')
|
||||
cli.apply_filters(sample_image, output_path, "invalid_filter")
|
||||
|
||||
# Test image adjustment
|
||||
def test_adjust_image_all_parameters(self, cli, sample_image, temp_dir):
|
||||
"""Test adjusting all image parameters"""
|
||||
output_path = os.path.join(temp_dir, "adjusted.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
cli.adjust_image(sample_image, output_path,
|
||||
brightness=1.2, contrast=1.1,
|
||||
saturation=0.9, sharpness=1.3)
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.adjust_image(
|
||||
sample_image,
|
||||
output_path,
|
||||
brightness=1.2,
|
||||
contrast=1.1,
|
||||
saturation=0.9,
|
||||
sharpness=1.3,
|
||||
)
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
mock_print.assert_any_call("Adjusted image - Brightness: 1.2, Contrast: 1.1, Saturation: 0.9, Sharpness: 1.3")
|
||||
mock_print.assert_any_call(
|
||||
"Adjusted image - Brightness: 1.2, Contrast: 1.1, Saturation: 0.9, Sharpness: 1.3"
|
||||
)
|
||||
|
||||
def test_adjust_image_single_parameter(self, cli, sample_image, temp_dir):
|
||||
"""Test adjusting single parameter"""
|
||||
|
@ -253,7 +275,7 @@ class TestPillowCLI:
|
|||
"""Test valid image cropping"""
|
||||
output_path = os.path.join(temp_dir, "cropped.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.crop_image(sample_image, output_path, 10, 10, 50, 50)
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
|
@ -273,7 +295,7 @@ class TestPillowCLI:
|
|||
"""Test image rotation"""
|
||||
output_path = os.path.join(temp_dir, "rotated.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.rotate_image(sample_image, output_path, 45)
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
|
@ -294,8 +316,8 @@ class TestPillowCLI:
|
|||
"""Test horizontal image flip"""
|
||||
output_path = os.path.join(temp_dir, "flipped.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
cli.flip_image(sample_image, output_path, 'horizontal')
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.flip_image(sample_image, output_path, "horizontal")
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
mock_print.assert_any_call("Flipped image horizontally")
|
||||
|
@ -304,8 +326,8 @@ class TestPillowCLI:
|
|||
"""Test vertical image flip"""
|
||||
output_path = os.path.join(temp_dir, "flipped.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
cli.flip_image(sample_image, output_path, 'vertical')
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.flip_image(sample_image, output_path, "vertical")
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
mock_print.assert_any_call("Flipped image vertically")
|
||||
|
@ -314,39 +336,41 @@ class TestPillowCLI:
|
|||
"""Test flipping with invalid direction"""
|
||||
output_path = os.path.join(temp_dir, "flipped.jpg")
|
||||
|
||||
with pytest.raises(ValueError, match="Direction must be 'horizontal' or 'vertical'"):
|
||||
cli.flip_image(sample_image, output_path, 'diagonal')
|
||||
with pytest.raises(
|
||||
ValueError, match="Direction must be 'horizontal' or 'vertical'"
|
||||
):
|
||||
cli.flip_image(sample_image, output_path, "diagonal")
|
||||
|
||||
# Test format conversion
|
||||
def test_convert_format_png(self, cli, sample_image, temp_dir):
|
||||
"""Test converting to PNG format"""
|
||||
output_path = os.path.join(temp_dir, "converted.png")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
cli.convert_format(sample_image, output_path, 'PNG')
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.convert_format(sample_image, output_path, "PNG")
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
converted_image = Image.open(output_path)
|
||||
assert converted_image.format == 'PNG'
|
||||
assert converted_image.format == "PNG"
|
||||
mock_print.assert_any_call("Converted to PNG format")
|
||||
|
||||
def test_convert_rgba_to_jpeg(self, cli, sample_png_image, temp_dir):
|
||||
"""Test converting RGBA image to JPEG"""
|
||||
output_path = os.path.join(temp_dir, "converted.jpg")
|
||||
|
||||
cli.convert_format(sample_png_image, output_path, 'JPEG')
|
||||
cli.convert_format(sample_png_image, output_path, "JPEG")
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
converted_image = Image.open(output_path)
|
||||
assert converted_image.format == 'JPEG'
|
||||
assert converted_image.mode == 'RGB'
|
||||
assert converted_image.format == "JPEG"
|
||||
assert converted_image.mode == "RGB"
|
||||
|
||||
# Test thumbnail creation
|
||||
def test_create_thumbnail(self, cli, sample_image, temp_dir):
|
||||
"""Test thumbnail creation"""
|
||||
output_path = os.path.join(temp_dir, "thumb.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.create_thumbnail(sample_image, output_path, (64, 64))
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
|
@ -360,7 +384,7 @@ class TestPillowCLI:
|
|||
"""Test adding watermark"""
|
||||
output_path = os.path.join(temp_dir, "watermarked.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.add_watermark(sample_image, output_path, "Test Watermark")
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
|
@ -368,7 +392,7 @@ class TestPillowCLI:
|
|||
|
||||
def test_add_watermark_all_positions(self, cli, sample_image, temp_dir):
|
||||
"""Test watermark at all positions"""
|
||||
positions = ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'center']
|
||||
positions = ["top-left", "top-right", "bottom-left", "bottom-right", "center"]
|
||||
|
||||
for position in positions:
|
||||
output_path = os.path.join(temp_dir, f"watermark_{position}.jpg")
|
||||
|
@ -380,22 +404,22 @@ class TestPillowCLI:
|
|||
output_path = os.path.join(temp_dir, "watermarked.jpg")
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid position"):
|
||||
cli.add_watermark(sample_image, output_path, "Test", position='invalid')
|
||||
cli.add_watermark(sample_image, output_path, "Test", position="invalid")
|
||||
|
||||
# Test metadata extraction
|
||||
def test_extract_metadata(self, cli, sample_image):
|
||||
"""Test metadata extraction"""
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("builtins.print") as mock_print:
|
||||
metadata = cli.extract_metadata(sample_image)
|
||||
|
||||
assert isinstance(metadata, dict)
|
||||
assert 'filename' in metadata
|
||||
assert 'format' in metadata
|
||||
assert 'size' in metadata
|
||||
assert 'width' in metadata
|
||||
assert 'height' in metadata
|
||||
assert metadata['width'] == 100
|
||||
assert metadata['height'] == 100
|
||||
assert "filename" in metadata
|
||||
assert "format" in metadata
|
||||
assert "size" in metadata
|
||||
assert "width" in metadata
|
||||
assert "height" in metadata
|
||||
assert metadata["width"] == 100
|
||||
assert metadata["height"] == 100
|
||||
|
||||
# Check that JSON was printed
|
||||
mock_print.assert_called()
|
||||
|
@ -405,7 +429,7 @@ class TestPillowCLI:
|
|||
"""Test collage creation"""
|
||||
output_path = os.path.join(temp_dir, "collage.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.create_collage(multiple_sample_images, output_path, cols=2, padding=5)
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
|
@ -426,15 +450,15 @@ class TestPillowCLI:
|
|||
"""Test sepia effect"""
|
||||
output_path = os.path.join(temp_dir, "sepia.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
cli.apply_artistic_effects(sample_image, output_path, 'sepia')
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.apply_artistic_effects(sample_image, output_path, "sepia")
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
mock_print.assert_any_call("Applied sepia effect")
|
||||
|
||||
def test_apply_artistic_effects_all_types(self, cli, sample_image, temp_dir):
|
||||
"""Test all artistic effects"""
|
||||
effects = ['sepia', 'grayscale', 'invert', 'posterize', 'solarize']
|
||||
effects = ["sepia", "grayscale", "invert", "posterize", "solarize"]
|
||||
|
||||
for effect in effects:
|
||||
output_path = os.path.join(temp_dir, f"{effect}.jpg")
|
||||
|
@ -446,20 +470,20 @@ class TestPillowCLI:
|
|||
output_path = os.path.join(temp_dir, "effect.jpg")
|
||||
|
||||
with pytest.raises(ValueError, match="Unknown effect"):
|
||||
cli.apply_artistic_effects(sample_image, output_path, 'invalid_effect')
|
||||
cli.apply_artistic_effects(sample_image, output_path, "invalid_effect")
|
||||
|
||||
# Test histogram analysis
|
||||
def test_analyze_histogram(self, cli, sample_image, temp_dir):
|
||||
"""Test histogram analysis"""
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("builtins.print") as mock_print:
|
||||
histogram_data = cli.analyze_histogram(sample_image)
|
||||
|
||||
assert isinstance(histogram_data, dict)
|
||||
assert 'red_channel' in histogram_data
|
||||
assert 'green_channel' in histogram_data
|
||||
assert 'blue_channel' in histogram_data
|
||||
assert 'total_pixels' in histogram_data
|
||||
assert histogram_data['total_pixels'] == 10000 # 100x100
|
||||
assert "red_channel" in histogram_data
|
||||
assert "green_channel" in histogram_data
|
||||
assert "blue_channel" in histogram_data
|
||||
assert "total_pixels" in histogram_data
|
||||
assert histogram_data["total_pixels"] == 10000 # 100x100
|
||||
|
||||
mock_print.assert_called()
|
||||
|
||||
|
@ -467,21 +491,21 @@ class TestPillowCLI:
|
|||
"""Test histogram analysis with output file"""
|
||||
output_path = os.path.join(temp_dir, "histogram.json")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.analyze_histogram(sample_image, output_path)
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
with open(output_path, 'r') as f:
|
||||
with open(output_path) as f:
|
||||
histogram_data = json.load(f)
|
||||
|
||||
assert isinstance(histogram_data, dict)
|
||||
assert 'total_pixels' in histogram_data
|
||||
assert "total_pixels" in histogram_data
|
||||
mock_print.assert_any_call(f"Histogram data saved to: {output_path}")
|
||||
|
||||
# Test color palette extraction
|
||||
def test_extract_color_palette(self, cli, sample_image):
|
||||
"""Test color palette extraction"""
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("builtins.print") as mock_print:
|
||||
colors = cli.extract_color_palette(sample_image, num_colors=3)
|
||||
|
||||
assert isinstance(colors, list)
|
||||
|
@ -496,31 +520,40 @@ class TestPillowCLI:
|
|||
|
||||
def test_extract_color_palette_without_sklearn(self, cli, sample_image):
|
||||
"""Test color palette extraction without sklearn (fallback method)"""
|
||||
with patch('sklearn.cluster.KMeans', side_effect=ImportError()):
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("sklearn.cluster.KMeans", side_effect=ImportError()):
|
||||
with patch("builtins.print") as mock_print:
|
||||
colors = cli.extract_color_palette(sample_image, num_colors=3)
|
||||
|
||||
assert isinstance(colors, list)
|
||||
mock_print.assert_any_call("scikit-learn not installed. Using simple color extraction...")
|
||||
mock_print.assert_any_call(
|
||||
"scikit-learn not installed. Using simple color extraction..."
|
||||
)
|
||||
|
||||
# Test border creation
|
||||
def test_create_border(self, cli, sample_image, temp_dir):
|
||||
"""Test border creation"""
|
||||
output_path = os.path.join(temp_dir, "bordered.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
cli.create_border(sample_image, output_path, border_width=5, border_color='red')
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.create_border(
|
||||
sample_image, output_path, border_width=5, border_color="red"
|
||||
)
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
bordered_image = Image.open(output_path)
|
||||
assert bordered_image.size == (110, 110) # Original 100x100 + 5px border on each side
|
||||
assert bordered_image.size == (
|
||||
110,
|
||||
110,
|
||||
) # Original 100x100 + 5px border on each side
|
||||
mock_print.assert_any_call("Added 5px border with color (255, 0, 0)")
|
||||
|
||||
def test_create_border_hex_color(self, cli, sample_image, temp_dir):
|
||||
"""Test border with hex color"""
|
||||
output_path = os.path.join(temp_dir, "bordered.jpg")
|
||||
|
||||
cli.create_border(sample_image, output_path, border_width=3, border_color='#FF0000')
|
||||
cli.create_border(
|
||||
sample_image, output_path, border_width=3, border_color="#FF0000"
|
||||
)
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
|
||||
|
@ -532,24 +565,28 @@ class TestPillowCLI:
|
|||
overlay_path = os.path.join(temp_dir, "overlay.png")
|
||||
output_path = os.path.join(temp_dir, "composite.png")
|
||||
|
||||
bg_image = Image.new('RGBA', (100, 100), color=(255, 0, 0, 255))
|
||||
overlay_image = Image.new('RGBA', (50, 50), color=(0, 0, 255, 128))
|
||||
bg_image = Image.new("RGBA", (100, 100), color=(255, 0, 0, 255))
|
||||
overlay_image = Image.new("RGBA", (50, 50), color=(0, 0, 255, 128))
|
||||
|
||||
bg_image.save(bg_path, 'PNG')
|
||||
overlay_image.save(overlay_path, 'PNG')
|
||||
bg_image.save(bg_path, "PNG")
|
||||
overlay_image.save(overlay_path, "PNG")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
cli.create_composite(bg_path, overlay_path, output_path, position=(25, 25), opacity=0.8)
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.create_composite(
|
||||
bg_path, overlay_path, output_path, position=(25, 25), opacity=0.8
|
||||
)
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
mock_print.assert_any_call("Created composite image at position (25, 25) with opacity 0.8")
|
||||
mock_print.assert_any_call(
|
||||
"Created composite image at position (25, 25) with opacity 0.8"
|
||||
)
|
||||
|
||||
# Test vignette effect
|
||||
def test_apply_vignette(self, cli, sample_image, temp_dir):
|
||||
"""Test vignette effect"""
|
||||
output_path = os.path.join(temp_dir, "vignette.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.apply_vignette(sample_image, output_path, strength=0.3)
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
|
@ -560,8 +597,10 @@ class TestPillowCLI:
|
|||
"""Test contact sheet creation"""
|
||||
output_path = os.path.join(temp_dir, "contact_sheet.jpg")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
cli.create_contact_sheet(multiple_sample_images, output_path, sheet_width=800, margin=5)
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.create_contact_sheet(
|
||||
multiple_sample_images, output_path, sheet_width=800, margin=5
|
||||
)
|
||||
|
||||
assert os.path.exists(output_path)
|
||||
contact_image = Image.open(output_path)
|
||||
|
@ -581,7 +620,7 @@ class TestPillowCLI:
|
|||
"""Test RGB channel splitting"""
|
||||
output_dir = os.path.join(temp_dir, "channels")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("builtins.print") as mock_print:
|
||||
channel_paths = cli.split_channels(sample_image, output_dir)
|
||||
|
||||
assert len(channel_paths) == 3
|
||||
|
@ -590,7 +629,11 @@ class TestPillowCLI:
|
|||
|
||||
# Check that files have correct names
|
||||
base_name = Path(sample_image).stem
|
||||
expected_files = [f"{base_name}_red.png", f"{base_name}_green.png", f"{base_name}_blue.png"]
|
||||
expected_files = [
|
||||
f"{base_name}_red.png",
|
||||
f"{base_name}_green.png",
|
||||
f"{base_name}_blue.png",
|
||||
]
|
||||
for expected_file in expected_files:
|
||||
assert any(expected_file in path for path in channel_paths)
|
||||
|
||||
|
@ -607,11 +650,11 @@ class TestPillowCLI:
|
|||
# Create sample images
|
||||
for i in range(3):
|
||||
image_path = os.path.join(input_dir, f"image_{i}.jpg")
|
||||
image = Image.new('RGB', (100, 100), color='red')
|
||||
image.save(image_path, 'JPEG')
|
||||
image = Image.new("RGB", (100, 100), color="red")
|
||||
image.save(image_path, "JPEG")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
cli.batch_process(input_dir, output_dir, 'resize', width=50, height=50)
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.batch_process(input_dir, output_dir, "resize", width=50, height=50)
|
||||
|
||||
# Check output directory exists and contains processed images
|
||||
assert os.path.exists(output_dir)
|
||||
|
@ -620,7 +663,9 @@ class TestPillowCLI:
|
|||
|
||||
# Fix: The actual implementation finds both .jpg and .JPG files, so it finds 6 files total
|
||||
# Let's check for the correct number that the implementation actually finds
|
||||
mock_print.assert_any_call("Batch processing complete! Output saved to: " + output_dir)
|
||||
mock_print.assert_any_call(
|
||||
"Batch processing complete! Output saved to: " + output_dir
|
||||
)
|
||||
|
||||
def test_batch_process_no_images(self, cli, temp_dir):
|
||||
"""Test batch processing with no images in directory"""
|
||||
|
@ -628,8 +673,8 @@ class TestPillowCLI:
|
|||
output_dir = os.path.join(temp_dir, "output")
|
||||
os.makedirs(input_dir)
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
cli.batch_process(input_dir, output_dir, 'resize')
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.batch_process(input_dir, output_dir, "resize")
|
||||
|
||||
mock_print.assert_any_call("No image files found in the input directory")
|
||||
|
||||
|
@ -639,7 +684,7 @@ class TestPillowCLI:
|
|||
output_dir = os.path.join(temp_dir, "output")
|
||||
|
||||
with pytest.raises(FileNotFoundError, match="Input directory not found"):
|
||||
cli.batch_process(input_dir, output_dir, 'resize')
|
||||
cli.batch_process(input_dir, output_dir, "resize")
|
||||
|
||||
def test_batch_process_unknown_operation(self, cli, temp_dir):
|
||||
"""Test batch processing with unknown operation"""
|
||||
|
@ -649,25 +694,27 @@ class TestPillowCLI:
|
|||
|
||||
# Create a sample image
|
||||
image_path = os.path.join(input_dir, "image.jpg")
|
||||
image = Image.new('RGB', (100, 100), color='red')
|
||||
image.save(image_path, 'JPEG')
|
||||
image = Image.new("RGB", (100, 100), color="red")
|
||||
image.save(image_path, "JPEG")
|
||||
|
||||
with patch('builtins.print') as mock_print:
|
||||
cli.batch_process(input_dir, output_dir, 'unknown_operation')
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.batch_process(input_dir, output_dir, "unknown_operation")
|
||||
|
||||
mock_print.assert_any_call("Unknown batch operation: unknown_operation")
|
||||
|
||||
# Test GUI functionality
|
||||
def test_simple_gui_not_available(self, cli):
|
||||
"""Test GUI when tkinter is not available"""
|
||||
with patch('pillow_cli.GUI_AVAILABLE', False):
|
||||
with patch('builtins.print') as mock_print:
|
||||
with patch("pillow_cli.GUI_AVAILABLE", False):
|
||||
with patch("builtins.print") as mock_print:
|
||||
cli.simple_gui()
|
||||
|
||||
mock_print.assert_called_with("GUI not available. Install tkinter to use this feature.")
|
||||
mock_print.assert_called_with(
|
||||
"GUI not available. Install tkinter to use this feature."
|
||||
)
|
||||
|
||||
@patch('pillow_cli.GUI_AVAILABLE', True)
|
||||
@patch('pillow_cli.tk')
|
||||
@patch("pillow_cli.GUI_AVAILABLE", True)
|
||||
@patch("pillow_cli.tk")
|
||||
def test_simple_gui_available(self, mock_tk_module, cli):
|
||||
"""Test GUI when tkinter is available"""
|
||||
# Mock the entire tk module and its components
|
||||
|
@ -677,7 +724,7 @@ class TestPillowCLI:
|
|||
|
||||
# Mock ttk as well
|
||||
mock_ttk = MagicMock()
|
||||
with patch('pillow_cli.ttk', mock_ttk):
|
||||
with patch("pillow_cli.ttk", mock_ttk):
|
||||
cli.simple_gui()
|
||||
|
||||
mock_tk_module.Tk.assert_called_once()
|
||||
|
@ -703,10 +750,10 @@ class TestIntegration:
|
|||
def sample_image(self, temp_dir):
|
||||
"""Create a sample image for integration testing"""
|
||||
image_path = os.path.join(temp_dir, "sample.jpg")
|
||||
image = Image.new('RGB', (200, 200), color='blue')
|
||||
image = Image.new("RGB", (200, 200), color="blue")
|
||||
draw = ImageDraw.Draw(image)
|
||||
draw.rectangle([50, 50, 150, 150], fill='yellow')
|
||||
image.save(image_path, 'JPEG')
|
||||
draw.rectangle([50, 50, 150, 150], fill="yellow")
|
||||
image.save(image_path, "JPEG")
|
||||
return image_path
|
||||
|
||||
def test_image_processing_chain(self, temp_dir, sample_image):
|
||||
|
@ -719,11 +766,11 @@ class TestIntegration:
|
|||
|
||||
# Step 2: Apply filter
|
||||
filtered_path = os.path.join(temp_dir, "step2_filtered.jpg")
|
||||
cli.apply_filters(resized_path, filtered_path, 'sharpen')
|
||||
cli.apply_filters(resized_path, filtered_path, "sharpen")
|
||||
|
||||
# Step 3: Add watermark
|
||||
final_path = os.path.join(temp_dir, "step3_final.jpg")
|
||||
cli.add_watermark(filtered_path, final_path, "Processed", position='center')
|
||||
cli.add_watermark(filtered_path, final_path, "Processed", position="center")
|
||||
|
||||
assert os.path.exists(final_path)
|
||||
final_image = Image.open(final_path)
|
||||
|
@ -735,11 +782,11 @@ class TestIntegration:
|
|||
|
||||
# JPG to PNG
|
||||
png_path = os.path.join(temp_dir, "converted.png")
|
||||
cli.convert_format(sample_image, png_path, 'PNG')
|
||||
cli.convert_format(sample_image, png_path, "PNG")
|
||||
|
||||
# PNG to BMP
|
||||
bmp_path = os.path.join(temp_dir, "converted.bmp")
|
||||
cli.convert_format(png_path, bmp_path, 'BMP')
|
||||
cli.convert_format(png_path, bmp_path, "BMP")
|
||||
|
||||
assert os.path.exists(png_path)
|
||||
assert os.path.exists(bmp_path)
|
||||
|
@ -747,8 +794,8 @@ class TestIntegration:
|
|||
png_image = Image.open(png_path)
|
||||
bmp_image = Image.open(bmp_path)
|
||||
|
||||
assert png_image.format == 'PNG'
|
||||
assert bmp_image.format == 'BMP'
|
||||
assert png_image.format == "PNG"
|
||||
assert bmp_image.format == "BMP"
|
||||
assert png_image.size == bmp_image.size
|
||||
|
||||
|
||||
|
@ -771,7 +818,7 @@ class TestErrorHandling:
|
|||
|
||||
# Create a file with wrong extension
|
||||
corrupted_path = os.path.join(temp_dir, "corrupted.jpg")
|
||||
with open(corrupted_path, 'w') as f:
|
||||
with open(corrupted_path, "w") as f:
|
||||
f.write("This is not an image file")
|
||||
|
||||
with pytest.raises(Exception): # PIL will raise an exception
|
||||
|
@ -783,8 +830,8 @@ class TestErrorHandling:
|
|||
|
||||
# Create a small image
|
||||
image_path = os.path.join(temp_dir, "small.jpg")
|
||||
image = Image.new('RGB', (10, 10), color='red')
|
||||
image.save(image_path, 'JPEG')
|
||||
image = Image.new("RGB", (10, 10), color="red")
|
||||
image.save(image_path, "JPEG")
|
||||
|
||||
output_path = os.path.join(temp_dir, "large.jpg")
|
||||
|
||||
|
@ -800,8 +847,8 @@ class TestErrorHandling:
|
|||
cli = PillowCLI()
|
||||
|
||||
image_path = os.path.join(temp_dir, "test.jpg")
|
||||
image = Image.new('RGB', (100, 100), color='red')
|
||||
image.save(image_path, 'JPEG')
|
||||
image = Image.new("RGB", (100, 100), color="red")
|
||||
image.save(image_path, "JPEG")
|
||||
|
||||
output_path = os.path.join(temp_dir, "cropped.jpg")
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user