diff --git a/README.md b/README.md index 769f8f3..ea72ef0 100644 --- a/README.md +++ b/README.md @@ -22,20 +22,18 @@ Select - [x] Free-form selection (Lasso) - [ ] Polygon selection - [x] Crop -- [ ] Resize - -Rotate -- [x] Rotate right 90 degrees -- [x] Rotate Left 90 degrees -- [x] Flip vertical -- [x] Flip horizontal  +- [x] Resize +- [ ] Rotate right 90 degrees +- [ ] Rotate Left 90 degrees +- [ ] Flip vertical +- [ ] Flip horizontal  Tools menu - [ ] Zoom (Zoom In, Zoom Out) - [ ] Erase -- [x] Color Picker +- [ ] Color Picker - [ ] Paint brushes (with different textures/patterns) -- [x] Text box +- [ ] Text box Filters - [ ] Gaussian filter @@ -50,10 +48,11 @@ Shapes menu Colors menu - [ ] Color pallet -- [x] Size of brush +- [ ] Size of brush -### Optional feature to design: -- [x] Snapchat filters +### Optional features +- [x] Camera +- [x] Snapchat camera filters - [ ] Smart Scissors - [ ] Geofilters diff --git a/src/GUI.py b/src/GUI.py index 68d2e4c..c4c9653 100644 --- a/src/GUI.py +++ b/src/GUI.py @@ -1,5 +1,5 @@ import tkinter as tk -from tkinter import filedialog, colorchooser +from tkinter import filedialog, colorchooser, messagebox from PIL import ImageTk, ImageDraw, Image from ImageContainer import ImageContainer from ImageManipulation.ManipulationList import * @@ -100,6 +100,17 @@ class GUI: image_menu = tk.Menu(menu, tearoff=0) menu.add_cascade(label="Image", menu=image_menu) + + # Select submenu + select_menu = tk.Menu(image_menu, tearoff=0) + image_menu.add_cascade(label="Select", menu=select_menu) + select_menu.add_command(label="Rectangular Selection", command=lambda: self._setRectangularSelection()) + select_menu.add_command(label="Free-form Selection (Lasso)", command=lambda: self._setLassoSelection()) + select_menu.add_command(label="Circular Selection", command=lambda: self._setCircularSelection()) + + image_menu.add_separator() + image_menu.add_command(label="Crop", command=lambda: self._cropImage()) + image_menu.add_command(label="Resize", command=lambda: self._resizeImage()) filter_menu = tk.Menu(menu, tearoff=0) menu.add_cascade(label="Filter", menu=filter_menu) clipboard_menu = tk.Menu(menu, tearoff=0) @@ -305,6 +316,55 @@ class GUI: """Toggle selection shape between rectangle, circle, and lasso.""" if self._areaSelectionHandler: self._areaSelectionHandler.toggle_selection_shape() + + def _setRectangularSelection(self) -> None: + """Set selection to rectangular mode.""" + if self._areaSelectionHandler: + self._areaSelectionHandler.set_rectangular_selection() + + def _setCircularSelection(self) -> None: + """Set selection to circular mode.""" + if self._areaSelectionHandler: + self._areaSelectionHandler.set_circular_selection() + + def _setLassoSelection(self) -> None: + """Set selection to lasso (free-form) mode.""" + if self._areaSelectionHandler: + self._areaSelectionHandler.set_lasso_selection() + + def _cropImage(self) -> None: + """Crop the image to the selected rectangular area. Starts selection mode if no selection exists.""" + if not self._areaSelectionHandler: + return + + # If there's already a valid selection, crop immediately + if (self._areaSelectionHandler.selection_area and + self._areaSelectionHandler.selection_area.is_active and + self._areaSelectionHandler.selection_area.is_valid()): + self._areaSelectionHandler.crop_to_selection() + else: + # Set up rectangular selection mode and callback + self._areaSelectionHandler.set_rectangular_selection() + self._areaSelectionHandler.set_pending_operation_callback( + lambda: self._areaSelectionHandler.crop_to_selection() + ) + + def _resizeImage(self) -> None: + """Resize the image to match the selected rectangular area dimensions. Starts selection mode if no selection exists.""" + if not self._areaSelectionHandler: + return + + # If there's already a valid selection, resize immediately + if (self._areaSelectionHandler.selection_area and + self._areaSelectionHandler.selection_area.is_active and + self._areaSelectionHandler.selection_area.is_valid()): + self._areaSelectionHandler.resize_to_selection_dimensions() + else: + # Set up rectangular selection mode and callback + self._areaSelectionHandler.set_rectangular_selection() + self._areaSelectionHandler.set_pending_operation_callback( + lambda: self._areaSelectionHandler.resize_to_selection_dimensions() + ) def _onMouseClick(self, event) -> None: """Handle mouse click for selection or brush.""" diff --git a/src/utils/area_selection.py b/src/utils/area_selection.py index 78deaee..f00536b 100644 --- a/src/utils/area_selection.py +++ b/src/utils/area_selection.py @@ -57,6 +57,9 @@ class AreaSelectionHandler: self._selectionRectangle = None self._lassoPath = [] # List of canvas coordinates for lasso drawing self._lassoLine = None # Canvas line item for lasso path + + # Pending operation callback (called when selection is completed) + self._pendingOperationCallback = None def toggle_selection_mode(self) -> None: """Toggle interactive selection mode on/off.""" @@ -65,6 +68,7 @@ class AreaSelectionHandler: self._root.config(cursor="crosshair") else: self._root.config(cursor="") + self._pendingOperationCallback = None # Clear pending callback when disabling selection mode self.clear_selection() def toggle_selection_shape(self) -> None: @@ -86,6 +90,44 @@ class AreaSelectionHandler: if self._selectionArea.is_active and self._selectionArea.is_valid(): self._render_image() + def set_selection_shape(self, shape: str) -> None: + """Set the selection shape directly. + + Args: + shape: One of "rectangle", "circle", or "lasso" + """ + if self._selectionArea is None: + return + if shape in ("rectangle", "circle", "lasso"): + self._selectionArea.set_shape(shape) + self._selectionType = shape + # Enable selection mode if not already enabled + if not self._selectionMode: + self.toggle_selection_mode() + # Refresh selection drawing if active + if self._selectionArea.is_active and self._selectionArea.is_valid(): + self._render_image() + + def set_rectangular_selection(self) -> None: + """Set selection to rectangular mode and enable selection.""" + self.set_selection_shape("rectangle") + + def set_circular_selection(self) -> None: + """Set selection to circular mode and enable selection.""" + self.set_selection_shape("circle") + + def set_lasso_selection(self) -> None: + """Set selection to lasso (free-form) mode and enable selection.""" + self.set_selection_shape("lasso") + + def set_pending_operation_callback(self, callback: Optional[Callable]) -> None: + """Set a callback to be executed when a selection is completed. + + Args: + callback: Function to call when selection is completed, or None to clear + """ + self._pendingOperationCallback = callback + def on_mouse_click(self, event) -> bool: """Handle mouse click for selection. @@ -212,6 +254,12 @@ class AreaSelectionHandler: # Redraw the selection highlight to ensure it's visible self._render_image() + + # Check if there's a pending operation callback and execute it + if self._pendingOperationCallback and self._selectionArea.is_valid(): + callback = self._pendingOperationCallback + self._pendingOperationCallback = None # Clear callback to prevent multiple calls + callback() return True # Handle rectangle/circle mode @@ -236,6 +284,12 @@ class AreaSelectionHandler: ) # Redraw the selection highlight to ensure it's visible self._render_image() + + # Check if there's a pending operation callback and execute it + if self._pendingOperationCallback and self._selectionArea.is_valid(): + callback = self._pendingOperationCallback + self._pendingOperationCallback = None # Clear callback to prevent multiple calls + callback() return True def on_right_click(self, event, get_default_params: Callable) -> bool: @@ -391,6 +445,56 @@ class AreaSelectionHandler: # Silently handle errors - crop operation failed pass + def resize_to_selection_dimensions(self) -> None: + """Resize the entire image to match the selection area dimensions.""" + current_image = self._get_current_image() + if current_image is None or not self._selectionArea or not self._selectionArea.is_active: + return + + # Get the selection coordinates + coords = self._selectionArea.get_coordinates() + if not coords: + return + + left, top, right, bottom = coords + + # Calculate dimensions from selection + width = right - left + height = bottom - top + + # Validate dimensions + if width <= 0 or height <= 0: + return + + # Get the original image + original_image = current_image.getImage() + if original_image is None: + return + + try: + # Take undo snapshot + current_image.snapshot() + + # Resize the entire image to the selection dimensions + from utils.image_utils import pil_to_cv2, cv2_to_pil + import cv2 + + cv_img = pil_to_cv2(original_image) + resized = cv2.resize(cv_img, (int(width), int(height)), interpolation=cv2.INTER_AREA) + resized_image = cv2_to_pil(resized) + + # Update the current image + current_image._imageData = resized_image + + # Clear selection before re-rendering + self.clear_selection() + + # Re-render the image (without selection) + self._render_image() + except Exception as e: + # Silently handle errors - resize operation failed + pass + def apply_manipulation_to_selection(self, manipulation, get_default_params: Callable) -> None: """Apply the specified manipulation to the selected area only.""" current_image = self._get_current_image()