# -*- coding: utf-8 -*-
"""
Created on Thu Nov 20 04:37:59 2025

@author: mstep
"""
DEBUG = False

#%%
from PIL import Image, ImageDraw

def draw_blurred_shape(image, xmin, ymin, xmax, ymax, shape_type,
                       radius=5, step=0.5):
    '''Draw a shape with blurred edges.'''
    # Make a Draw object to draw the shapes.
    dr = ImageDraw.Draw(image)

    # Draw the shapes.
    r = 0
    while r <= radius:
        fill = (0, 0, 0, int(255 * r / radius))
        rect = (xmin + r, ymin + r, xmax - r, ymax - r)
        if shape_type == 'Rectangle':
            dr.rectangle(rect, fill=fill)
        else:
            dr.ellipse(rect, fill=fill)
        r += step

    # Fill the final shape.
    fill = (0, 0, 0, 255)
    if shape_type == 'Rectangle':
        dr.rectangle(rect, fill=fill)
    else:
        dr.ellipse(rect, fill=fill)

#%%
from PIL import Image, ImageDraw

class Filter:
    '''A public class to represent filters.'''
    def __init__(self, kernel, weight=None, offset=0):
        # Make sure the kernel has an odd number of rows and columns.
        if len(kernel) % 2 == 0:
            raise ValueError('The kernel must have an odd number of rows')
        if len(kernel[0]) % 2 == 0:
            raise ValueError('The kernel must have an odd number of columns')

        # Save values.
        self.kernel = kernel
        if weight is None:
            self.normalize()
        else:
            self.weight = weight
        self.offset = offset

    def get_total(self):
        '''Return the kernel's total weight.'''
        total = 0
        for row in self.kernel:
            total += sum(row)
        return total

    def normalize(self):
        '''Set the filter's weight equal to the sum of the kernel's values.'''
        self.weight = self.get_total()

    def __str__(self):
        '''Return a string represention of the filter.'''
        return f'{self.kernel}\nweight = {self.weight}, offset = {self.offset}'

    def apply(self, image):
        '''Apply the filter to a PIL image.'''
        # Get the pixels.
        pixels = image.load()

        # Make a copy.
        result_image = image.copy()

        # Fill the image with a default color if desired.
        dr = ImageDraw.Draw(result_image)
        dr.rectangle((0, 0, result_image.width, result_image.height),
                     fill='white')

        # Load the result pixels.
        result_pixels = result_image.load()

        # Find the number of rows and columns, and the middle row and column.
        num_rows = len(self.kernel)
        num_cols = len(self.kernel[0])
        mid_y = num_rows // 2
        mid_x = num_cols // 2

        # Get bounds for pixels to which we can apply the kernel.
        min_y = mid_y
        max_y = image.height - mid_y
        min_x = mid_x
        max_x = image.width - mid_x

        # Apply the filter.
        for y in range(min_y, max_y):
            for x in range(min_x, max_x):
                # Apply the filters to pixel[x, y]
                # Process the pixels under the kernel.
                red = green = blue = 0
                for dy in range(num_rows):
                    pix_y = y + dy - mid_y
                    for dx in range(num_cols):
                        pix_x = x + dx - mid_x
                        pix_red, pix_green, pix_blue = pixels[pix_x, pix_y]

                        red   += self.kernel[dy][dx] * pix_red
                        green += self.kernel[dy][dx] * pix_green
                        blue  += self.kernel[dy][dx] * pix_blue

                # Divide by the weight, add the offset, and
                # make sure the result is between 0 and 255.
                red = int(self.offset + red / self.weight)
                red = max(min(red, 255), 0)

                green = int(self.offset + green / self.weight)
                green = max(min(green, 255), 0)

                blue = int(self.offset + blue / self.weight)
                blue = max(min(blue, 255), 0)

                # Set the new pixel's value.
                result_pixels[x, y] = (red, green, blue)

        # Return the result.
        return result_image

    # Filters.
    @classmethod
    def box_filter(cls, size):
        '''Create a {size}x{size} low-pass filter.'''
        return cls([[1 for _ in range(size)] for _ in range(size)])

#%%
from PIL import Image, ImageDraw

def blur_area(image, shape_type, radius, filter_size, xmin, ymin, xmax, ymax):
    '''Pixelate the indicated area.'''
    # Expand the selected area to include the blurred area plus
    # a margin so the filter will fit.
    margin = filter_size // 2
    xmin -= radius + margin
    ymin -= radius + margin
    xmax += radius + margin
    ymax += radius + margin

    # Copy the image's selected area.
    rect = (xmin, ymin, xmax, ymax)
    rect_image = image.crop(rect)

    # Blur the area.
    rect_image = Filter.box_filter(filter_size).apply(rect_image)

    # Make a mask image.
    wid = rect_image.width
    hgt = rect_image.height
    mask = Image.new('RGBA', (wid, hgt), color=None)
    draw_blurred_shape(mask, margin, margin, wid - margin, hgt - margin,
                       shape_type, radius)

    # Debgging: Save the mask.
    if DEBUG:
        mask.save('mask.png')

    # Copy the blurred area back onto the original image using the mask.
    image.paste(rect_image, (xmin, ymin), mask)

    # Debugging: Outline the selected area and the expanded area.
    if DEBUG:
        dr = ImageDraw.Draw(image)
        if shape_type == 'Rectangle':
            dr.rectangle(rect, outline='yellow')
        else:
            dr.ellipse(rect, outline='yellow')

#%%
import tkinter as tk
import tkinter.messagebox as msg
from tkinter.filedialog import askopenfilename, asksaveasfilename

class ScrolledFrame(tk.Frame):
    def __init__(self, frame, *args, **kwargs):
        tk.Frame.__init__(self, frame, *args, **kwargs)

        # Make row 0 and column 0 expand.
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        # Scrollbars.
        self.v_scroll = tk.Scrollbar(self, orient=tk.VERTICAL)
        self.v_scroll.grid(row=0, column=1, sticky=tk.NSEW)

        self.h_scroll = tk.Scrollbar(self, orient=tk.HORIZONTAL)
        self.h_scroll.grid(row=1, column=0, sticky=tk.NSEW)

        # Canvas.
        self.canvas = tk.Canvas(self,
            xscrollcommand=self.h_scroll.set,
            yscrollcommand=self.v_scroll.set)
        self.canvas.grid(row=0, column=0, sticky=tk.NSEW)

        # Configure the canvas.
        self.v_scroll.config(command=self.canvas.yview)
        self.h_scroll.config(command=self.canvas.xview)

        # Frame.
        self.frame = tk.Frame(self.canvas)
        self.frame_window = self.canvas.create_window(0, 0,
            window=self.frame, anchor=tk.NW)


    def configure_frame(self):
        self.frame.update_idletasks()
        width = self.frame.winfo_reqwidth()
        height = self.frame.winfo_reqheight()
        self.canvas.config(scrollregion=(0, 0, width, height))

        # Bind the mouse wheel to scroll the canvas.
        for child in widget_and_descendants(self.canvas):
            child.bind('<MouseWheel>', self.wheel_scroll)
            #child.bind('<Button-4>', self.wheel_scroll)
            #child.bind('<Button-5>', self.wheel_scroll)

    def wheel_scroll(self, event):
        ''' Scroll the canvas.'''
        # This is the Windows version.
        # Divide event.delta by some number to slow scrolling. Some people
        # recommend dividing by 120 for Windows, but I left it alone to make
        # it scroll about the same speed when the mouse is over the scrollbar.
        units = -int(event.delta / 30)
        self.canvas.yview_scroll(units, 'units')

def widget_and_descendants(parent):
    '''Return this widget and all of its descendants.'''
    results = [parent]
    for child in parent.winfo_children():
        results += widget_and_descendants(child)
    return results

#%%
import tkinter as tk
from PIL import Image, ImageTk

class BlurAreaApp:
    def __init__(self):
        # Make the main interface.
        self.window = tk.Tk()
        self.window.title('blur_area')
        self.window.protocol('WM_DELETE_WINDOW', self.kill_callback)
        self.window.geometry('500x400')

        # Build the File menu.
        self.build_menus()

        # Make a ScrolledFrame.
        self.canvas = None
        self.scrolled_frame = ScrolledFrame(self.window)
        self.scrolled_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        # Make the canvas.
        self.canvas = tk.Canvas(self.scrolled_frame.frame, cursor='crosshair',
                                width=10, height=10)
        self.canvas.grid(row=0, column=0)

        # Configure the frame. (Do this after adding all contained widgets.)
        self.scrolled_frame.configure_frame()

        # Initially we are not selecting an area.
        self.start_point = None

        # Display the window.
        self.window.focus_force()
        self.window.mainloop()

    def build_menus(self):
        '''Build the menu bar.'''
        menu_bar = tk.Menu(self.window)
        self.window.config(menu=menu_bar)
        self.build_file_menu(menu_bar)
        self.build_blur_menu(menu_bar)

    def build_file_menu(self, menu_bar):
        '''Build the File menu.'''
        file_menu = tk.Menu(menu_bar, tearoff=False)
        menu_bar.add_cascade(label='File', menu=file_menu)

        # Note: You must save the photo image in a persistent
        # variable so tkinter can redraw it when needed.

        # Note: You need to include the compound parameter
        # if you want to use both images and text. That value
        # tells where to put the image relative to the text.

        # Open
        self.open_photo_image = ImageTk.PhotoImage(file='open.png')
        file_menu.add_command(label='Open...', accelerator='Ctrl+O',
                              compound='left', image=self.open_photo_image,
                              command=self.mnu_open)
        self.window.bind('<Control-o>', lambda event: self.mnu_open())

        # Save As
        self.save_as_photo_image = ImageTk.PhotoImage(file='save_as.png')
        file_menu.add_command(label='Save As...', accelerator='Ctrl+O',
                              compound='left', image=self.save_as_photo_image,
                              command=self.mnu_save_as)
        self.window.bind('<Control-s>', lambda event: self.mnu_save_as())

        # Reset.
        file_menu.add_separator()
        self.restore_photo_image = ImageTk.PhotoImage(file='restore_image.png')
        file_menu.add_command(label='Restore Image', accelerator='Ctrl+R',
                              compound='left', image=self.restore_photo_image,
                              command=self.mnu_restore)
        self.window.bind('<Control-r>', lambda event: self.mnu_restore())

        # Exit
        file_menu.add_separator()
        self.exit_photo_image = ImageTk.PhotoImage(file='exit.png')
        file_menu.add_command(label='Exit', compound='left',
            command=self.kill_callback, image=self.exit_photo_image)

    def mnu_open(self):
        '''Let the user select the file to open.'''
        filetypes = [
            ('Image Files', '*.png *.jpg *.gif *.bmp'),
            ('All Files', '*.*')]
        filename = askopenfilename(filetypes=filetypes)
        if len(filename) > 0:
            # Load the image.
            try:
                # Load the image.
                self.original_image = Image.open(filename).convert('RGB')
                self.pil_image = self.original_image.copy()
                self.show_image()

                # Size the canvas to fit.
                self.canvas.config(width=self.pil_image.width,
                                   height=self.pil_image.height)

                # Display the image on the canvas.
                self.canvas.create_image(0, 0, anchor=tk.NW,
                                         image=self.tk_image)

                # Track mouse events on the label.
                self.canvas.bind("<Button-1>", self.mouse_down)
                self.canvas.bind("<B1-Motion>", self.mouse_move)
                self.canvas.bind("<ButtonRelease-1>", self.mouse_up)

            except Exception as e:
                msg.showerror('File Open Error', e)

            # Configure the frame.
            self.scrolled_frame.configure_frame()

    def mnu_save_as(self):
        '''Let the user select the file where we should save the result.'''
        filetypes = [
            ('Image Files', '*.png *.jpg *.gif *.bmp'),
            ('All Files', '*.*')]
        filename = asksaveasfilename(filetypes=filetypes,
                                     defaultextension=".jpg")
        if len(filename) > 0:
            try:
                self.pil_image.save(filename)
            except Exception as e:
                msg.showerror('File Save Error', e)

    def mnu_restore(self):
        '''Undo all changes to the loaded image.'''
        self.pil_image = self.original_image.copy()
        self.show_image()

    def build_blur_menu(self, menu_bar):
        '''Build the Blur menu.'''
        block_menu = tk.Menu(menu_bar, tearoff=False)
        menu_bar.add_cascade(label='Blur', menu=block_menu)

        self.shape_type_var = tk.StringVar(value='Ellipse')
        block_menu.add_radiobutton(label='Ellipse', value='Ellipse',
                                   variable=self.shape_type_var)
        block_menu.add_radiobutton(label='Rectangle', value='Rectangle',
                                   variable=self.shape_type_var)
        block_menu.add_separator()
        self.radius_var = tk.IntVar(value=5)
        block_menu.add_radiobutton(label='Radius 5', value=5,
                                   variable=self.radius_var)
        block_menu.add_radiobutton(label='Radius 10', value=10,
                                   variable=self.radius_var)
        block_menu.add_radiobutton(label='Radius 20', value=20,
                                   variable=self.radius_var)
        block_menu.add_separator()
        self.filter_size_var = tk.IntVar(value=7)
        block_menu.add_radiobutton(label='7x7 Kernel', value=7,
                                   variable=self.filter_size_var)
        block_menu.add_radiobutton(label='15x15 Kernel', value=15,
                                   variable=self.filter_size_var)
        block_menu.add_radiobutton(label='25x25 Kernel', value=25,
                                   variable=self.filter_size_var)

    def kill_callback(self):
        self.window.destroy()

    def mouse_down(self, event):
        '''Start drawing a selection rectangle.'''
        self.start_point = self.end_point = (event.x, event.y)
        self.selection_rectangle = self.canvas.create_rectangle(
            self.start_point, self.start_point, outline='red', fill='',
            width=3)

    def mouse_move(self, event):
        '''Adjust the selection rectangle.'''
        self.end_point = (event.x, event.y)
        self.canvas.coords(self.selection_rectangle, self.start_point,
                           self.end_point)

    def mouse_up(self, event):
        '''Pixelate the selected area.'''
        if self.start_point is None:
            return
        xmin = min(self.start_point[0], self.end_point[0])
        xmax = max(self.start_point[0], self.end_point[0])
        ymin = min(self.start_point[1], self.end_point[1])
        ymax = max(self.start_point[1], self.end_point[1])
        shape_type = self.shape_type_var.get()
        radius = self.radius_var.get()
        filter_size = self.filter_size_var.get()
        blur_area(self.pil_image, shape_type, radius, filter_size,
                  xmin, ymin, xmax, ymax)

        if DEBUG:
            # Draw the blurred area.
            dr = ImageDraw.Draw(self.pil_image)
            rect = (xmin, ymin, xmax, ymax)
            if shape_type == 'Rectangle':
                dr.rectangle(rect, outline='red')
            else:
                dr.ellipse(rect, outline='red')

        # Display the result.
        self.show_image()

        # We are no longer selecting an area.
        self.start_point = self.end_point = self.selection_rectangle = None

    def show_image(self):
        '''Display self.pil_image.'''
        self.canvas.delete(tk.ALL)
        self.tk_image = ImageTk.PhotoImage(self.pil_image)
        self.canvas.create_image(0, 0, anchor=tk.NW, image=self.tk_image)


#%%

BlurAreaApp()
print('Done')
