adding requirements for image menu; integrating area selection with crop and resize functionalities

This commit is contained in:
vb
2025-11-06 16:10:48 +01:00
parent db4fc94385
commit 5aba41a4f9
3 changed files with 176 additions and 13 deletions

View File

@@ -22,20 +22,18 @@ Select
- [x] Free-form selection (Lasso) - [x] Free-form selection (Lasso)
- [ ] Polygon selection - [ ] Polygon selection
- [x] Crop - [x] Crop
- [ ] Resize - [x] Resize
- [ ] Rotate right 90 degrees
Rotate - [ ] Rotate Left 90 degrees
- [x] Rotate right 90 degrees - [ ] Flip vertical
- [x] Rotate Left 90 degrees - [ ] Flip horizontal 
- [x] Flip vertical
- [x] Flip horizontal 
Tools menu Tools menu
- [ ] Zoom (Zoom In, Zoom Out) - [ ] Zoom (Zoom In, Zoom Out)
- [ ] Erase - [ ] Erase
- [x] Color Picker - [ ] Color Picker
- [ ] Paint brushes (with different textures/patterns) - [ ] Paint brushes (with different textures/patterns)
- [x] Text box - [ ] Text box
Filters Filters
- [ ] Gaussian filter - [ ] Gaussian filter
@@ -50,10 +48,11 @@ Shapes menu
Colors menu Colors menu
- [ ] Color pallet - [ ] Color pallet
- [x] Size of brush - [ ] Size of brush
### Optional feature to design: ### Optional features
- [x] Snapchat filters - [x] Camera
- [x] Snapchat camera filters
- [ ] Smart Scissors - [ ] Smart Scissors
- [ ] Geofilters - [ ] Geofilters

View File

@@ -1,5 +1,5 @@
import tkinter as tk import tkinter as tk
from tkinter import filedialog, colorchooser from tkinter import filedialog, colorchooser, messagebox
from PIL import ImageTk, ImageDraw, Image from PIL import ImageTk, ImageDraw, Image
from ImageContainer import ImageContainer from ImageContainer import ImageContainer
from ImageManipulation.ManipulationList import * from ImageManipulation.ManipulationList import *
@@ -100,6 +100,17 @@ class GUI:
image_menu = tk.Menu(menu, tearoff=0) image_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Image", menu=image_menu) 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) filter_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Filter", menu=filter_menu) menu.add_cascade(label="Filter", menu=filter_menu)
clipboard_menu = tk.Menu(menu, tearoff=0) clipboard_menu = tk.Menu(menu, tearoff=0)
@@ -306,6 +317,55 @@ class GUI:
if self._areaSelectionHandler: if self._areaSelectionHandler:
self._areaSelectionHandler.toggle_selection_shape() 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: def _onMouseClick(self, event) -> None:
"""Handle mouse click for selection or brush.""" """Handle mouse click for selection or brush."""
# Handle brush mode - delegate to handler # Handle brush mode - delegate to handler

View File

@@ -58,6 +58,9 @@ class AreaSelectionHandler:
self._lassoPath = [] # List of canvas coordinates for lasso drawing self._lassoPath = [] # List of canvas coordinates for lasso drawing
self._lassoLine = None # Canvas line item for lasso path 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: def toggle_selection_mode(self) -> None:
"""Toggle interactive selection mode on/off.""" """Toggle interactive selection mode on/off."""
self._selectionMode = not self._selectionMode self._selectionMode = not self._selectionMode
@@ -65,6 +68,7 @@ class AreaSelectionHandler:
self._root.config(cursor="crosshair") self._root.config(cursor="crosshair")
else: else:
self._root.config(cursor="") self._root.config(cursor="")
self._pendingOperationCallback = None # Clear pending callback when disabling selection mode
self.clear_selection() self.clear_selection()
def toggle_selection_shape(self) -> None: def toggle_selection_shape(self) -> None:
@@ -86,6 +90,44 @@ class AreaSelectionHandler:
if self._selectionArea.is_active and self._selectionArea.is_valid(): if self._selectionArea.is_active and self._selectionArea.is_valid():
self._render_image() 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: def on_mouse_click(self, event) -> bool:
"""Handle mouse click for selection. """Handle mouse click for selection.
@@ -212,6 +254,12 @@ class AreaSelectionHandler:
# Redraw the selection highlight to ensure it's visible # Redraw the selection highlight to ensure it's visible
self._render_image() 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 return True
# Handle rectangle/circle mode # Handle rectangle/circle mode
@@ -236,6 +284,12 @@ class AreaSelectionHandler:
) )
# Redraw the selection highlight to ensure it's visible # Redraw the selection highlight to ensure it's visible
self._render_image() 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 return True
def on_right_click(self, event, get_default_params: Callable) -> bool: def on_right_click(self, event, get_default_params: Callable) -> bool:
@@ -391,6 +445,56 @@ class AreaSelectionHandler:
# Silently handle errors - crop operation failed # Silently handle errors - crop operation failed
pass 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: def apply_manipulation_to_selection(self, manipulation, get_default_params: Callable) -> None:
"""Apply the specified manipulation to the selected area only.""" """Apply the specified manipulation to the selected area only."""
current_image = self._get_current_image() current_image = self._get_current_image()