Code for How to Make an Image Editor in Python Tutorial

gui_image_editor.py

import ttkbootstrap as ttk
from tkinter import filedialog
from tkinter.messagebox import showerror, askyesno
from tkinter import colorchooser
from PIL import Image, ImageOps, ImageTk, ImageFilter, ImageGrab


# defining global variables
WIDTH = 750
HEIGHT = 560
file_path = ""
pen_size = 3
pen_color = "black"



# function to open the image file
def open_image():
    global file_path
    file_path = filedialog.askopenfilename(title="Open Image File", filetypes=[("Image Files", "*.jpg;*.jpeg;*.png;*.gif;*.bmp")])
    if file_path:
        global image, photo_image
        image = Image.open(file_path)
        new_width = int((WIDTH / 2))
        image = image.resize((new_width, HEIGHT), Image.LANCZOS)
            
        image = ImageTk.PhotoImage(image)
        canvas.create_image(0, 0, anchor="nw", image=image)
        

# a global variable for checking the flip state of the image
is_flipped = False

def flip_image():
    try:
        global image, photo_image, is_flipped
        if not is_flipped:
            # open the image and flip it left and right
            image = Image.open(file_path).transpose(Image.FLIP_LEFT_RIGHT)
            is_flipped = True
        else:
            # reset the image to its original state
            image = Image.open(file_path)
            is_flipped = False
        # resize the image to fit the canvas
        new_width = int((WIDTH / 2))
        image = image.resize((new_width, HEIGHT), Image.LANCZOS)
        # convert the PIL image to a Tkinter PhotoImage and display it on the canvas
        photo_image = ImageTk.PhotoImage(image)
        canvas.create_image(0, 0, anchor="nw", image=photo_image)

    except:
        showerror(title='Flip Image Error', message='Please select an image to flip!')


# global variable for tracking rotation angle
rotation_angle = 0

# function for rotating the image
def rotate_image():
    try:
        global image, photo_image, rotation_angle
        # open the image and rotate it
        
        image = Image.open(file_path)
        new_width = int((WIDTH / 2))
        image = image.resize((new_width, HEIGHT), Image.LANCZOS)
        rotated_image = image.rotate(rotation_angle + 90)
        rotation_angle += 90
        # reset image if angle is a multiple of 360 degrees
        if rotation_angle % 360 == 0:
            rotation_angle = 0
            image = Image.open(file_path)
            image = image.resize((new_width, HEIGHT), Image.LANCZOS)
            rotated_image = image
        # convert the PIL image to a Tkinter PhotoImage and display it on the canvas
        photo_image = ImageTk.PhotoImage(rotated_image)
        canvas.create_image(0, 0, anchor="nw", image=photo_image)
    
    except:
        showerror(title='Rotate Image Error', message='Please select an image to rotate!')




# function for applying filters to the opened image file
def apply_filter(filter):
    global image, photo_image
    try:
        # check if the image has been flipped or rotated
        if is_flipped:
            # flip the original image left and right
            flipped_image = Image.open(file_path).transpose(Image.FLIP_LEFT_RIGHT)
            # rotate the flipped image
            rotated_image = flipped_image.rotate(rotation_angle)
            # apply the filter to the rotated image
            if filter == "Black and White":
                rotated_image = ImageOps.grayscale(rotated_image)
            elif filter == "Blur":
                rotated_image = rotated_image.filter(ImageFilter.BLUR)

            elif filter == "Contour":
                rotated_image = rotated_image.filter(ImageFilter.CONTOUR)

            elif filter == "Detail":
                rotated_image = rotated_image.filter(ImageFilter.DETAIL)

            elif filter == "Emboss":
                rotated_image = rotated_image.filter(ImageFilter.EMBOSS)

            elif filter == "Edge Enhance":
                rotated_image = rotated_image.filter(ImageFilter.EDGE_ENHANCE)

            elif filter == "Sharpen":
                rotated_image = rotated_image.filter(ImageFilter.SHARPEN)

            elif filter == "Smooth":
                rotated_image = rotated_image.filter(ImageFilter.SMOOTH)
                        
            else:
                rotated_image = Image.open(file_path).transpose(Image.FLIP_LEFT_RIGHT).rotate(rotation_angle)

        elif rotation_angle != 0:
            # rotate the original image
            rotated_image = Image.open(file_path).rotate(rotation_angle)
            # apply the filter to the rotated image
            if filter == "Black and White":
                rotated_image = ImageOps.grayscale(rotated_image)
                
            elif filter == "Blur":
                rotated_image = rotated_image.filter(ImageFilter.BLUR)

            elif filter == "Contour":
                rotated_image = rotated_image.filter(ImageFilter.CONTOUR)

            elif filter == "Detail":
                rotated_image = rotated_image.filter(ImageFilter.DETAIL)

            elif filter == "Emboss":
                rotated_image = rotated_image.filter(ImageFilter.EMBOSS)

            elif filter == "Edge Enhance":
                rotated_image = rotated_image.filter(ImageFilter.EDGE_ENHANCE)

            elif filter == "Sharpen":
                rotated_image = rotated_image.filter(ImageFilter.SHARPEN)

            elif filter == "Smooth":
                rotated_image = rotated_image.filter(ImageFilter.SMOOTH)
                
            else:
                rotated_image = Image.open(file_path).rotate(rotation_angle)
                
        else:
            # apply the filter to the original image
            image = Image.open(file_path)
            if filter == "Black and White":
                image = ImageOps.grayscale(image)

            elif filter == "Blur":
                image = image.filter(ImageFilter.BLUR)

            elif filter == "Sharpen":
                image = image.filter(ImageFilter.SHARPEN)

            elif filter == "Smooth":
                image = image.filter(ImageFilter.SMOOTH)

            elif filter == "Emboss":
                image = image.filter(ImageFilter.EMBOSS)

            elif filter == "Detail":
                image = image.filter(ImageFilter.DETAIL)


            elif filter == "Edge Enhance":
                image = image.filter(ImageFilter.EDGE_ENHANCE)

            elif filter == "Contour":
                image = image.filter(ImageFilter.CONTOUR)


            rotated_image = image
        
        # resize the rotated/flipped image to fit the canvas
        new_width = int((WIDTH / 2))
        rotated_image = rotated_image.resize((new_width, HEIGHT), Image.LANCZOS)
        # convert the PIL image to a Tkinter PhotoImage and display it on the canvas
        photo_image = ImageTk.PhotoImage(rotated_image)
        canvas.create_image(0, 0, anchor="nw", image=photo_image)
        
    except:
        showerror(title='Error', message='Please select an image first!')




# function for drawing lines on the opened image
def draw(event):
    global file_path
    if file_path:
        x1, y1 = (event.x - pen_size), (event.y - pen_size)
        x2, y2 = (event.x + pen_size), (event.y + pen_size)
        canvas.create_oval(x1, y1, x2, y2, fill=pen_color, outline="", width=pen_size, tags="oval")


# function for changing the pen color
def change_color():
    global pen_color
    pen_color = colorchooser.askcolor(title="Select Pen Color")[1]



# function for erasing lines on the opened image
def erase_lines():
    global file_path
    if file_path:
        canvas.delete("oval")




def save_image():
    global file_path, is_flipped, rotation_angle

    if file_path:
        # create a new PIL Image object from the canvas
        image = ImageGrab.grab(bbox=(canvas.winfo_rootx(), canvas.winfo_rooty(), canvas.winfo_rootx() + canvas.winfo_width(), canvas.winfo_rooty() + canvas.winfo_height()))

        # check if the image has been flipped or rotated
        if is_flipped or rotation_angle % 360 != 0:
            # Resize and rotate the image
            new_width = int((WIDTH / 2))
            image = image.resize((new_width, HEIGHT), Image.LANCZOS)
            if is_flipped:
                image = image.transpose(Image.FLIP_LEFT_RIGHT)
            if rotation_angle % 360 != 0:
                image = image.rotate(rotation_angle)

            # update the file path to include the modifications in the file name
            file_path = file_path.split(".")[0] + "_mod.jpg"

        # apply any filters to the image before saving
        filter = filter_combobox.get()
        if filter:
            if filter == "Black and White":
                image = ImageOps.grayscale(image)

            elif filter == "Blur":
                image = image.filter(ImageFilter.BLUR)

            elif filter == "Sharpen":
                image = image.filter(ImageFilter.SHARPEN)

            elif filter == "Smooth":
                image = image.filter(ImageFilter.SMOOTH)

            elif filter == "Emboss":
                image = image.filter(ImageFilter.EMBOSS)

            elif filter == "Detail":
                image = image.filter(ImageFilter.DETAIL)

            elif filter == "Edge Enhance":
                image = image.filter(ImageFilter.EDGE_ENHANCE)
                
            elif filter == "Contour":
                image = image.filter(ImageFilter.CONTOUR)

            # update the file path to include the filter in the file name
            file_path = file_path.split(".")[0] + "_" + filter.lower().replace(" ", "_") + ".jpg"

        # open file dialog to select save location and file type
        file_path = filedialog.asksaveasfilename(defaultextension=".jpg")

        if file_path:
            if askyesno(title='Save Image', message='Do you want to save this image?'):
                # save the image to a file
                image.save(file_path)




root = ttk.Window(themename="cosmo")
root.title("Image Editor")
root.geometry("510x580+300+110")
root.resizable(0, 0)
icon = ttk.PhotoImage(file='icon.png')
root.iconphoto(False, icon)

# the left frame to contain the 4 buttons
left_frame = ttk.Frame(root, width=200, height=600)
left_frame.pack(side="left", fill="y")

# the right canvas for displaying the image
canvas = ttk.Canvas(root, width=WIDTH, height=HEIGHT)
canvas.pack()
# binding the Canvas to the B1-Motion event
canvas.bind("<B1-Motion>", draw)

# label
filter_label = ttk.Label(left_frame, text="Select Filter:", background="white")
filter_label.pack(padx=0, pady=2)

# a list of filters
image_filters = ["Contour", "Black and White", "Blur", "Detail", "Emboss", "Edge Enhance", "Sharpen", "Smooth"]

# combobox for the filters
filter_combobox = ttk.Combobox(left_frame, values=image_filters, width=15)
filter_combobox.pack(padx=10, pady=5)

# binding the apply_filter function to the combobox
filter_combobox.bind("<<ComboboxSelected>>", lambda event: apply_filter(filter_combobox.get()))

# loading the icons for the 4 buttons
image_icon = ttk.PhotoImage(file = 'add.png').subsample(12, 12)
flip_icon = ttk.PhotoImage(file = 'flip.png').subsample(12, 12)
rotate_icon = ttk.PhotoImage(file = 'rotate.png').subsample(12, 12)
color_icon = ttk.PhotoImage(file = 'color.png').subsample(12, 12)
erase_icon = ttk.PhotoImage(file = 'erase.png').subsample(12, 12)
save_icon = ttk.PhotoImage(file = 'saved.png').subsample(12, 12)

# button for adding/opening the image file
image_button = ttk.Button(left_frame, image=image_icon, bootstyle="light", command=open_image)
image_button.pack(pady=5)
# button for flipping the image file
flip_button = ttk.Button(left_frame, image=flip_icon, bootstyle="light", command=flip_image)
flip_button.pack(pady=5)
# button for rotating the image file
rotate_button = ttk.Button(left_frame, image=rotate_icon, bootstyle="light", command=rotate_image)
rotate_button.pack(pady=5)
# button for choosing pen color
color_button = ttk.Button(left_frame, image=color_icon, bootstyle="light", command=change_color)
color_button.pack(pady=5)
# button for erasing the lines drawn over the image file
erase_button = ttk.Button(left_frame, image=erase_icon, bootstyle="light", command=erase_lines)
erase_button.pack(pady=5)
# button for saving the image file
save_button = ttk.Button(left_frame, image=save_icon, bootstyle="light", command=save_image)
save_button.pack(pady=5)

root.mainloop()