diff --git a/README.md b/README.md index d73603a..47228b7 100644 --- a/README.md +++ b/README.md @@ -29,17 +29,17 @@ Image menu - [x] Flip horizontal  Tools menu -- [ ] Zoom (Zoom In, Zoom Out) -- [ ] Erase -- [ ] Color Picker -- [ ] Paint brushes (with different textures/patterns) -- [ ] Text box +- [x] Zoom (Zoom In, Zoom Out) +- [x] Erase +- [x] Color Picker +- [x] Paint brushes (with different textures/patterns) +- [x] Text box Filters -- [ ] Gaussian filter -- [ ] Sobel filter -- [ ] Binary filter -- [ ] Histogram thresholding  +- [x] Gaussian filter +- [x] Sobel filter +- [x] Binary filter +- [x] Histogram thresholding  Shapes menu - [ ] List of Shapes diff --git a/food.png b/food.png new file mode 100644 index 0000000..23fd33c Binary files /dev/null and b/food.png differ diff --git a/src/GUI.py b/src/GUI.py index 424bd17..b086776 100644 --- a/src/GUI.py +++ b/src/GUI.py @@ -69,7 +69,7 @@ class GUI: # Main window root = tk.Tk() root.title("Image Viewer") - root.geometry("1100x700") + root.geometry("1100x900") root.config(bg="white") icon = tk.PhotoImage(file='icon.png') root.tk.call('wm', 'iconphoto', root._w, icon) diff --git a/src/utils/brush.py b/src/utils/brush.py index 504860f..66663fc 100644 --- a/src/utils/brush.py +++ b/src/utils/brush.py @@ -44,8 +44,10 @@ class BrushHandler: # Brush state self._brushMode = False + self._eraseMode = False self._brushColor = "#000000" # Default black self._brushSize = 5 # Default brush size + self._brushShape = "circular" # "circular" or "rectangular" self._lastBrushX = None self._lastBrushY = None self._isDrawing = False @@ -54,6 +56,8 @@ class BrushHandler: # UI element references (will be set by GUI) self._colorPreview = None self._brushStatusLabel = None + self._brushToggleButton = None + self._eraseToggleButton = None self._colorSwatches = [] # List of color swatch buttons def create_ui_panel(self, parent_frame: tk.Frame) -> None: @@ -65,6 +69,44 @@ class BrushHandler: # Brush settings title tk.Label(parent_frame, text="Brush Settings", font=("Arial", 12, "bold"), bg="lightgray").pack(pady=15) + # Brush activation button + button_frame = tk.Frame(parent_frame, bg="lightgray") + button_frame.pack(pady=10, padx=15, fill="x") + + self._brushToggleButton = tk.Button( + button_frame, + text="Activate Brush", + command=self.toggle_brush_mode, + bg="#4CAF50", + fg="white", + font=("Arial", 10, "bold"), + relief="raised", + bd=2, + cursor="hand2", + width=20, + height=2 + ) + self._brushToggleButton.pack(pady=5) + + # Erase activation button + self._eraseToggleButton = tk.Button( + button_frame, + text="Activate Eraser", + command=self.toggle_erase_mode, + bg="#FF9800", + fg="white", + font=("Arial", 10, "bold"), + relief="raised", + bd=2, + cursor="hand2", + width=20, + height=2 + ) + self._eraseToggleButton.pack(pady=5) + + # Separator + tk.Frame(parent_frame, bg="lightgray", height=2, relief="sunken", bd=1).pack(pady=5, fill="x", padx=15) + # Color selection section color_section = tk.Frame(parent_frame, bg="lightgray") color_section.pack(pady=10, padx=15, fill="x") @@ -134,6 +176,39 @@ class BrushHandler: # Initialize color selection highlighting self._select_color(self._brushColor) + # Brush shape selection section + shape_section = tk.Frame(parent_frame, bg="lightgray") + shape_section.pack(pady=10, padx=15, fill="x") + + tk.Label(shape_section, text="Brush Shape:", font=("Arial", 10), bg="lightgray").pack(anchor="w") + + shape_frame = tk.Frame(shape_section, bg="lightgray") + shape_frame.pack(pady=5, fill="x") + + self._shapeVar = tk.StringVar(value=self._brushShape) + + circular_radio = tk.Radiobutton( + shape_frame, + text="Circular", + variable=self._shapeVar, + value="circular", + command=self._update_brush_shape, + bg="lightgray", + font=("Arial", 9) + ) + circular_radio.pack(side="left", padx=10) + + rectangular_radio = tk.Radiobutton( + shape_frame, + text="Rectangular", + variable=self._shapeVar, + value="rectangular", + command=self._update_brush_shape, + bg="lightgray", + font=("Arial", 9) + ) + rectangular_radio.pack(side="left", padx=10) + # Size selection section size_section = tk.Frame(parent_frame, bg="lightgray") size_section.pack(pady=10, padx=15, fill="x") @@ -161,9 +236,6 @@ class BrushHandler: info_frame = tk.Frame(parent_frame, bg="lightgray") info_frame.pack(pady=20, padx=15, fill="x") - tk.Label(info_frame, text="Brush Status:", font=("Arial", 10, "bold"), bg="lightgray").pack(anchor="w") - self._brushStatusLabel = tk.Label(info_frame, text="Inactive", fg="red", bg="lightgray", font=("Arial", 9)) - self._brushStatusLabel.pack(anchor="w", pady=5) def _select_color(self, color: str) -> None: """Select a color from the palette or custom color. @@ -188,10 +260,19 @@ class BrushHandler: # If custom color not in palette, all swatches remain raised (normal state) + def _update_brush_shape(self) -> None: + """Update the brush shape based on the selected radio button.""" + self._brushShape = self._shapeVar.get() + def toggle_brush_mode(self) -> None: """Toggle brush mode on/off.""" self._brushMode = not self._brushMode if self._brushMode: + # Disable erase mode when brush mode is enabled + if self._eraseMode: + self._eraseMode = False + if self._eraseToggleButton: + self._eraseToggleButton.config(text="Activate Eraser", bg="#FF9800") # Disable selection mode when brush mode is enabled if self._area_selection_handler and self._area_selection_handler.selection_mode: self._area_selection_handler.toggle_selection_mode() @@ -199,6 +280,9 @@ class BrushHandler: # Update status label if self._brushStatusLabel: self._brushStatusLabel.config(text="Active", fg="green") + # Update button + if self._brushToggleButton: + self._brushToggleButton.config(text="Deactivate Brush", bg="#f44336") else: self._root.config(cursor="") self._isDrawing = False @@ -207,14 +291,42 @@ class BrushHandler: # Update status label if self._brushStatusLabel: self._brushStatusLabel.config(text="Inactive", fg="red") + # Update button + if self._brushToggleButton: + self._brushToggleButton.config(text="Activate Brush", bg="#4CAF50") + + def toggle_erase_mode(self) -> None: + """Toggle erase mode on/off.""" + self._eraseMode = not self._eraseMode + if self._eraseMode: + # Disable brush mode when erase mode is enabled + if self._brushMode: + self._brushMode = False + if self._brushToggleButton: + self._brushToggleButton.config(text="Activate Brush", bg="#4CAF50") + # Disable selection mode when erase mode is enabled + if self._area_selection_handler and self._area_selection_handler.selection_mode: + self._area_selection_handler.toggle_selection_mode() + self._root.config(cursor="pencil") + # Update button + if self._eraseToggleButton: + self._eraseToggleButton.config(text="Deactivate Eraser", bg="#f44336") + else: + self._root.config(cursor="") + self._isDrawing = False + self._lastBrushX = None + self._lastBrushY = None + # Update button + if self._eraseToggleButton: + self._eraseToggleButton.config(text="Activate Eraser", bg="#FF9800") def on_mouse_click(self, event) -> bool: - """Handle mouse click for brush. + """Handle mouse click for brush or eraser. Returns: - True if the click was handled by brush, False otherwise + True if the click was handled by brush/eraser, False otherwise """ - if not self._brushMode: + if not self._brushMode and not self._eraseMode: return False current_image = self._get_current_image() @@ -233,12 +345,12 @@ class BrushHandler: return True def on_mouse_drag(self, event) -> bool: - """Handle mouse drag for brush. + """Handle mouse drag for brush or eraser. Returns: - True if the drag was handled by brush, False otherwise + True if the drag was handled by brush/eraser, False otherwise """ - if not self._brushMode or not self._isDrawing: + if (not self._brushMode and not self._eraseMode) or not self._isDrawing: return False self._draw_brush_line(self._lastBrushX, self._lastBrushY, event.x, event.y) @@ -247,12 +359,12 @@ class BrushHandler: return True def on_mouse_release(self, event) -> bool: - """Handle mouse release to stop brush. + """Handle mouse release to stop brush or eraser. Returns: - True if the release was handled by brush, False otherwise + True if the release was handled by brush/eraser, False otherwise """ - if not self._brushMode or not self._isDrawing: + if (not self._brushMode and not self._eraseMode) or not self._isDrawing: return False self._isDrawing = False @@ -287,16 +399,26 @@ class BrushHandler: img_x = max(0, min(img_x, img_width - 1)) img_y = max(0, min(img_y, img_height - 1)) - # Convert hex color to RGB tuple - hex_color = self._brushColor.lstrip('#') - rgb_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + # Determine color: white for erase mode, brush color for brush mode + if self._eraseMode: + rgb_color = (255, 255, 255) # White for eraser + else: + # Convert hex color to RGB tuple + hex_color = self._brushColor.lstrip('#') + rgb_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) # Draw on the image draw = ImageDraw.Draw(pil_image) - # Draw an ellipse (circle) for the brush point radius = self._brushSize // 2 - draw.ellipse([img_x - radius, img_y - radius, img_x + radius, img_y + radius], - fill=rgb_color, outline=rgb_color) + + if self._brushShape == "rectangular": + # Draw a rectangle for the brush point + draw.rectangle([img_x - radius, img_y - radius, img_x + radius, img_y + radius], + fill=rgb_color, outline=rgb_color) + else: + # Draw an ellipse (circle) for the brush point + draw.ellipse([img_x - radius, img_y - radius, img_x + radius, img_y + radius], + fill=rgb_color, outline=rgb_color) # Update the image current_image._imageData = pil_image @@ -336,23 +458,44 @@ class BrushHandler: img_x2 = max(0, min(img_x2, img_width - 1)) img_y2 = max(0, min(img_y2, img_height - 1)) - # Convert hex color to RGB tuple - hex_color = self._brushColor.lstrip('#') - rgb_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + # Determine color: white for erase mode, brush color for brush mode + if self._eraseMode: + rgb_color = (255, 255, 255) # White for eraser + else: + # Convert hex color to RGB tuple + hex_color = self._brushColor.lstrip('#') + rgb_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) # Draw on the image draw = ImageDraw.Draw(pil_image) - # Draw a line with rounded ends radius = self._brushSize // 2 - # Draw the line itself - draw.line([(img_x1, img_y1), (img_x2, img_y2)], fill=rgb_color, width=self._brushSize) - - # Draw rounded ends (circles at start and end) - draw.ellipse([img_x1 - radius, img_y1 - radius, img_x1 + radius, img_y1 + radius], - fill=rgb_color) - draw.ellipse([img_x2 - radius, img_y2 - radius, img_x2 + radius, img_y2 + radius], - fill=rgb_color) + if self._brushShape == "rectangular": + # For rectangular brush, draw multiple rectangles along the line + # Calculate the distance and number of steps + dx = img_x2 - img_x1 + dy = img_y2 - img_y1 + distance = ((dx ** 2 + dy ** 2) ** 0.5) + + if distance > 0: + steps = max(1, int(distance / (self._brushSize / 2))) + for i in range(steps + 1): + t = i / steps if steps > 0 else 0 + x = int(img_x1 + dx * t) + y = int(img_y1 + dy * t) + # Draw a rectangle at this point + draw.rectangle([x - radius, y - radius, x + radius, y + radius], + fill=rgb_color, outline=rgb_color) + else: + # Draw a line with rounded ends (circular brush) + # Draw the line itself + draw.line([(img_x1, img_y1), (img_x2, img_y2)], fill=rgb_color, width=self._brushSize) + + # Draw rounded ends (circles at start and end) + draw.ellipse([img_x1 - radius, img_y1 - radius, img_x1 + radius, img_y1 + radius], + fill=rgb_color) + draw.ellipse([img_x2 - radius, img_y2 - radius, img_x2 + radius, img_y2 + radius], + fill=rgb_color) # Update the image current_image._imageData = pil_image