Creating a Simple Image Editing Program: My Own Tool Made with Python and Automator!
1. Why Did I Create It?
I was tired of running heavy programs for editing photos every time for blog posts. Especially when I just needed simple rotation or cropping, I thought, "Do I really need to install all this just for this one thing?". The 'Preview' app that comes standard with macOS doesn't have image rotation, and the Photos app requires you to add images to the library before you can use it. Other apps on the market are mostly paid or have unnecessarily heavy features. So, with the help of GPT, I decided to write a simple script with Python and create a workflow with Automator that I could run immediately on my Mac!
2. Developing an Image Rotator with Python
Python can easily implement image processing through the OpenCV and Pillow libraries. Below are the core functions:
- Image Rotation: Angle adjustment with a slider.
- Crop: Select the desired area by dragging the mouse.
- Save: Save as a JPEG after editing.
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import cv2
import numpy as np
from PIL import Image, ImageTk
import time
import os
import sys
class ImageRotator:
def __init__(self, root):
self.root = root
self.root.title("Image Rotator")
self.root.geometry("800x600")
self.original_image = None
self.cached_image = None
self.display_image = None
self.current_angle = 0
self.last_update = time.time()
self.update_interval = 0.05
self.setup_ui()
# Handle command-line arguments
if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]):
self.load_image_from_path(sys.argv[1])
def setup_ui(self):
self.main_frame = ttk.Frame(self.root)
self.main_frame.pack(fill=tk.BOTH, expand=True)
self.toolbar = ttk.Frame(self.main_frame)
self.toolbar.pack(fill=tk.X)
ttk.Button(self.toolbar, text="Load Image", command=self.load_image).pack(side=tk.LEFT, padx=5, pady=5)
ttk.Button(self.toolbar, text="Reset", command=self.reset_image).pack(side=tk.LEFT, padx=5, pady=5)
ttk.Button(self.toolbar, text="Save", command=self.save_image).pack(side=tk.LEFT, padx=5, pady=5)
self.angle_label = ttk.Label(self.toolbar, text="Rotation Angle: 0°")
self.angle_label.pack(side=tk.RIGHT, padx=5)
self.canvas = tk.Canvas(self.main_frame, bg='gray')
self.canvas.pack(fill=tk.BOTH, expand=True)
self.rotation_slider = ttk.Scale(
self.main_frame,
from_=-180,
to=180,
orient='horizontal',
command=self.on_rotation_change
)
self.rotation_slider.pack(fill=tk.X, padx=10, pady=5)
self.canvas.bind("", self.on_button_press)
self.canvas.bind("", self.on_mouse_drag)
self.canvas.bind("", self.on_button_release)
def load_image(self):
file_path = filedialog.askopenfilename(
filetypes=[("Image files", "*.jpg *.jpeg *.png *.bmp")]
)
if file_path:
self.load_image_from_path(file_path)
def load_image_from_path(self, file_path):
try:
img = cv2.imread(file_path)
if img is None:
raise Exception("Could not load the image.")
self.source_path = file_path
self.original_image = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
self.cached_image = None
self.reset_image()
except Exception as e:
messagebox.showerror("Error", str(e))
def on_rotation_change(self, value):
if self.original_image is None or time.time() - self.last_update < self.update_interval:
return
try:
self.current_angle = float(value)
self.rotate_and_crop()
self.last_update = time.time()
except ValueError:
pass
def rotate_and_crop(self):
if self.original_image is None:
return
if self.cached_image is None:
self.cached_image = self.original_image.copy()
height, width = self.cached_image.shape[:2]
angle_rad = np.radians(self.current_angle % 180)
cos_a = np.abs(np.cos(angle_rad))
sin_a = np.abs(np.sin(angle_rad))
scale = 1 / min(cos_a + sin_a, max(cos_a, sin_a))
scaled_size = (int(width * scale), int(height * scale))
scaled_image = cv2.resize(self.cached_image, scaled_size, interpolation=cv2.INTER_LANCZOS4)
center = (scaled_size[0] // 2, scaled_size[1] // 2)
rotation_matrix = cv2.getRotationMatrix2D(center, -self.current_angle, 1.0)
rotated = cv2.warpAffine(scaled_image, rotation_matrix, scaled_size, flags=cv2.INTER_LANCZOS4, borderMode=cv2.BORDER_REFLECT)
start_x = (scaled_size[0] - width) // 2
start_y = (scaled_size[1] - height) // 2
self.display_image = rotated[start_y:start_y+height, start_x:start_x+width]
self.update_display()
def update_display(self):
if self.display_image is None:
return
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
if canvas_width <= 1:
canvas_width = self.root.winfo_width() - 20
if canvas_height <= 1:
canvas_height = self.root.winfo_height() - 100
image_ratio = self.display_image.shape[1] / self.display_image.shape[0]
canvas_ratio = canvas_width / canvas_height
if image_ratio > canvas_ratio:
new_width = canvas_width
new_height = int(canvas_width / image_ratio)
else:
new_height = canvas_height
new_width = int(canvas_height * image_ratio)
resized = cv2.resize(self.display_image, (new_width, new_height), interpolation=cv2.INTER_AREA)
self.photo = ImageTk.PhotoImage(Image.fromarray(resized))
self.canvas.delete("all")
self.canvas.create_image(canvas_width//2, canvas_height//2, image=self.photo, anchor="center")
self.angle_label.config(text=f"Rotation Angle: {self.current_angle:.1f}°")
def on_button_press(self, event):
self.start_x = event.x
self.start_y = event.y
self.rect = self.canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y, outline='red')
def on_mouse_drag(self, event):
self.canvas.coords(self.rect, self.start_x, self.start_y, event.x, event.y)
def on_button_release(self, event):
if self.rect:
x1, y1, x2, y2 = self.canvas.coords(self.rect)
self.canvas.delete(self.rect)
self.crop_image(x1, y1, x2, y2)
def crop_image(self, x1, y1, x2, y2):
if self.display_image is None:
return
canvas_width = self.canvas.winfo_width()
canvas_height = self.canvas.winfo_height()
image_width = self.display_image.shape[1]
image_height = self.display_image.shape[0]
x1 = int((x1 / canvas_width) * image_width)
y1 = int((y1 / canvas_height) * image_height)
x2 = int((x2 / canvas_width) * image_width)
y2 = int((y2 / canvas_height) * image_height)
self.display_image = self.display_image[y1:y2, x1:x2]
self.update_display()
def reset_image(self):
if self.original_image is not None:
self.current_angle = 0
self.rotation_slider.set(0)
self.display_image = self.original_image.copy()
self.cached_image = None
self.update_display()
def save_image(self):
if self.display_image is None or self.source_path is None:
return
try:
save_path = os.path.splitext(self.source_path)[0] + '.jpg'
save_image = cv2.cvtColor(self.display_image, cv2.COLOR_RGB2BGR)
cv2.imwrite(save_path, save_image, [cv2.IMWRITE_JPEG_QUALITY, 95])
messagebox.showinfo("Success", "Image saved!")
except Exception as e:
messagebox.showerror("Error", f"Save failed: {str(e)}")
if __name__ == "__main__":
root = tk.Tk()
app = ImageRotator(root)
root.mainloop()
3. Running in 1 Second with Automator
Running the script every time in the terminal is annoying, right? With Automator, you can run the program with just a right-click on the image in Finder!
- Setting up virtual environment:
python3 -m venv venv source venv/bin/activate pip install opencv-python numpy Pillow
- Create an Automator Quick Action:
- Workflow type: Select Quick Action
- In "Workflow receives current", specify image files
- Add the execution code in Run Shell Script.
4. Actual Use Review
Pros: No program installation, lightweight and fast, free!
Cons: No advanced features, but enough for basic editing.
Usage: Useful for cropping blog thumbnails, rotating photos for SNS, etc.
5. In Conclusion
I no longer need to run a heavy program for image editing! The combination of Python and Automator, created with GPT, may seem simple, but it has become a powerful tool that boosts productivity. Try adding your own custom functions like applying filters or inserting text!
If a simple tool can solve the hassle of everyday life, that's true efficiency.
I've included the full code and detailed instructions on the blog, so please refer to them! 😊
🚀 Pro Version Upgrade Tips
- Add an image filter function: Implement edge detection using the
cv2.filter2D()
function. - Batch processing function: Add a function to automatically crop all images in a folder.
- Set keyboard shortcuts: Connect the save function with 'Ctrl+S'.