Make a Simple Photo Editor for MacOS with Python! (Let's write Python code with GPT.)

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!

I created an image rotator using Python.


So, I wrote down the following requirements for GPT.
- I'm using macOS, and I don't have a lightweight program that provides a rotate function to adjust the horizontal level of photos. They are all paid or the programs are too heavy.
- The function of Apple's native Photos app is perfect, but the Photos app needs to have the photos in the library, and I don't want that. I want to use it lightly just with image files. Let's make this with Python. These are the features I need.
- I need to be able to open the image with that program. (Right-click on the mouse and open with that program)
- Basically, it must support cropping. It should be possible to adjust it by dragging the 4 corners of a rectangle with the mouse in a GUI. The ratio does not matter.
- Among the crop features, there should also be a rotate function, allowing movement of 1 degree left and right. The default should be 0, and it should be possible to drag the mouse in the - or + direction to move it by 1 degree.
- It must be in GUI form, and a preview of the file I uploaded must be available for cropping or rotating.
- The most important thing is that when there is blank space when rotating, it should also be autocropped while being zoomed in.

Then, GPT output the Python code.

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 Automator so that the script can be called from the Finder on image

  1. Setting up virtual environment:
        python3 -m venv venv
    source venv/bin/activate
    pip install opencv-python numpy Pillow
    
        
  2. 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.
      This is how it looks when the self-created image rotator is added to the Quick Action.

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.

A simple editor that lets you rotate and crop the loaded image.

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'.

Post a Comment

Previous Post Next Post