adding erase functionality; adding brush shape option on the brush settings canvas
This commit is contained in:
18
README.md
18
README.md
@@ -29,17 +29,17 @@ Image menu
|
|||||||
- [x] Flip horizontal
|
- [x] Flip horizontal
|
||||||
|
|
||||||
Tools menu
|
Tools menu
|
||||||
- [ ] Zoom (Zoom In, Zoom Out)
|
- [x] Zoom (Zoom In, Zoom Out)
|
||||||
- [ ] Erase
|
- [x] Erase
|
||||||
- [ ] Color Picker
|
- [x] Color Picker
|
||||||
- [ ] Paint brushes (with different textures/patterns)
|
- [x] Paint brushes (with different textures/patterns)
|
||||||
- [ ] Text box
|
- [x] Text box
|
||||||
|
|
||||||
Filters
|
Filters
|
||||||
- [ ] Gaussian filter
|
- [x] Gaussian filter
|
||||||
- [ ] Sobel filter
|
- [x] Sobel filter
|
||||||
- [ ] Binary filter
|
- [x] Binary filter
|
||||||
- [ ] Histogram thresholding
|
- [x] Histogram thresholding
|
||||||
|
|
||||||
Shapes menu
|
Shapes menu
|
||||||
- [ ] List of Shapes
|
- [ ] List of Shapes
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class GUI:
|
|||||||
# Main window
|
# Main window
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
root.title("Image Viewer")
|
root.title("Image Viewer")
|
||||||
root.geometry("1100x700")
|
root.geometry("1100x900")
|
||||||
root.config(bg="white")
|
root.config(bg="white")
|
||||||
icon = tk.PhotoImage(file='icon.png')
|
icon = tk.PhotoImage(file='icon.png')
|
||||||
root.tk.call('wm', 'iconphoto', root._w, icon)
|
root.tk.call('wm', 'iconphoto', root._w, icon)
|
||||||
|
|||||||
@@ -44,8 +44,10 @@ class BrushHandler:
|
|||||||
|
|
||||||
# Brush state
|
# Brush state
|
||||||
self._brushMode = False
|
self._brushMode = False
|
||||||
|
self._eraseMode = False
|
||||||
self._brushColor = "#000000" # Default black
|
self._brushColor = "#000000" # Default black
|
||||||
self._brushSize = 5 # Default brush size
|
self._brushSize = 5 # Default brush size
|
||||||
|
self._brushShape = "circular" # "circular" or "rectangular"
|
||||||
self._lastBrushX = None
|
self._lastBrushX = None
|
||||||
self._lastBrushY = None
|
self._lastBrushY = None
|
||||||
self._isDrawing = False
|
self._isDrawing = False
|
||||||
@@ -54,6 +56,8 @@ class BrushHandler:
|
|||||||
# UI element references (will be set by GUI)
|
# UI element references (will be set by GUI)
|
||||||
self._colorPreview = None
|
self._colorPreview = None
|
||||||
self._brushStatusLabel = None
|
self._brushStatusLabel = None
|
||||||
|
self._brushToggleButton = None
|
||||||
|
self._eraseToggleButton = None
|
||||||
self._colorSwatches = [] # List of color swatch buttons
|
self._colorSwatches = [] # List of color swatch buttons
|
||||||
|
|
||||||
def create_ui_panel(self, parent_frame: tk.Frame) -> None:
|
def create_ui_panel(self, parent_frame: tk.Frame) -> None:
|
||||||
@@ -65,6 +69,44 @@ class BrushHandler:
|
|||||||
# Brush settings title
|
# Brush settings title
|
||||||
tk.Label(parent_frame, text="Brush Settings", font=("Arial", 12, "bold"), bg="lightgray").pack(pady=15)
|
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 selection section
|
||||||
color_section = tk.Frame(parent_frame, bg="lightgray")
|
color_section = tk.Frame(parent_frame, bg="lightgray")
|
||||||
color_section.pack(pady=10, padx=15, fill="x")
|
color_section.pack(pady=10, padx=15, fill="x")
|
||||||
@@ -134,6 +176,39 @@ class BrushHandler:
|
|||||||
# Initialize color selection highlighting
|
# Initialize color selection highlighting
|
||||||
self._select_color(self._brushColor)
|
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 selection section
|
||||||
size_section = tk.Frame(parent_frame, bg="lightgray")
|
size_section = tk.Frame(parent_frame, bg="lightgray")
|
||||||
size_section.pack(pady=10, padx=15, fill="x")
|
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 = tk.Frame(parent_frame, bg="lightgray")
|
||||||
info_frame.pack(pady=20, padx=15, fill="x")
|
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:
|
def _select_color(self, color: str) -> None:
|
||||||
"""Select a color from the palette or custom color.
|
"""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)
|
# 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:
|
def toggle_brush_mode(self) -> None:
|
||||||
"""Toggle brush mode on/off."""
|
"""Toggle brush mode on/off."""
|
||||||
self._brushMode = not self._brushMode
|
self._brushMode = not self._brushMode
|
||||||
if 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
|
# Disable selection mode when brush mode is enabled
|
||||||
if self._area_selection_handler and self._area_selection_handler.selection_mode:
|
if self._area_selection_handler and self._area_selection_handler.selection_mode:
|
||||||
self._area_selection_handler.toggle_selection_mode()
|
self._area_selection_handler.toggle_selection_mode()
|
||||||
@@ -199,6 +280,9 @@ class BrushHandler:
|
|||||||
# Update status label
|
# Update status label
|
||||||
if self._brushStatusLabel:
|
if self._brushStatusLabel:
|
||||||
self._brushStatusLabel.config(text="Active", fg="green")
|
self._brushStatusLabel.config(text="Active", fg="green")
|
||||||
|
# Update button
|
||||||
|
if self._brushToggleButton:
|
||||||
|
self._brushToggleButton.config(text="Deactivate Brush", bg="#f44336")
|
||||||
else:
|
else:
|
||||||
self._root.config(cursor="")
|
self._root.config(cursor="")
|
||||||
self._isDrawing = False
|
self._isDrawing = False
|
||||||
@@ -207,14 +291,42 @@ class BrushHandler:
|
|||||||
# Update status label
|
# Update status label
|
||||||
if self._brushStatusLabel:
|
if self._brushStatusLabel:
|
||||||
self._brushStatusLabel.config(text="Inactive", fg="red")
|
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:
|
def on_mouse_click(self, event) -> bool:
|
||||||
"""Handle mouse click for brush.
|
"""Handle mouse click for brush or eraser.
|
||||||
|
|
||||||
Returns:
|
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
|
return False
|
||||||
|
|
||||||
current_image = self._get_current_image()
|
current_image = self._get_current_image()
|
||||||
@@ -233,12 +345,12 @@ class BrushHandler:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def on_mouse_drag(self, event) -> bool:
|
def on_mouse_drag(self, event) -> bool:
|
||||||
"""Handle mouse drag for brush.
|
"""Handle mouse drag for brush or eraser.
|
||||||
|
|
||||||
Returns:
|
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
|
return False
|
||||||
|
|
||||||
self._draw_brush_line(self._lastBrushX, self._lastBrushY, event.x, event.y)
|
self._draw_brush_line(self._lastBrushX, self._lastBrushY, event.x, event.y)
|
||||||
@@ -247,12 +359,12 @@ class BrushHandler:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def on_mouse_release(self, event) -> bool:
|
def on_mouse_release(self, event) -> bool:
|
||||||
"""Handle mouse release to stop brush.
|
"""Handle mouse release to stop brush or eraser.
|
||||||
|
|
||||||
Returns:
|
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
|
return False
|
||||||
|
|
||||||
self._isDrawing = False
|
self._isDrawing = False
|
||||||
@@ -287,16 +399,26 @@ class BrushHandler:
|
|||||||
img_x = max(0, min(img_x, img_width - 1))
|
img_x = max(0, min(img_x, img_width - 1))
|
||||||
img_y = max(0, min(img_y, img_height - 1))
|
img_y = max(0, min(img_y, img_height - 1))
|
||||||
|
|
||||||
# Convert hex color to RGB tuple
|
# Determine color: white for erase mode, brush color for brush mode
|
||||||
hex_color = self._brushColor.lstrip('#')
|
if self._eraseMode:
|
||||||
rgb_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
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 on the image
|
||||||
draw = ImageDraw.Draw(pil_image)
|
draw = ImageDraw.Draw(pil_image)
|
||||||
# Draw an ellipse (circle) for the brush point
|
|
||||||
radius = self._brushSize // 2
|
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
|
# Update the image
|
||||||
current_image._imageData = pil_image
|
current_image._imageData = pil_image
|
||||||
@@ -336,23 +458,44 @@ class BrushHandler:
|
|||||||
img_x2 = max(0, min(img_x2, img_width - 1))
|
img_x2 = max(0, min(img_x2, img_width - 1))
|
||||||
img_y2 = max(0, min(img_y2, img_height - 1))
|
img_y2 = max(0, min(img_y2, img_height - 1))
|
||||||
|
|
||||||
# Convert hex color to RGB tuple
|
# Determine color: white for erase mode, brush color for brush mode
|
||||||
hex_color = self._brushColor.lstrip('#')
|
if self._eraseMode:
|
||||||
rgb_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
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 on the image
|
||||||
draw = ImageDraw.Draw(pil_image)
|
draw = ImageDraw.Draw(pil_image)
|
||||||
# Draw a line with rounded ends
|
|
||||||
radius = self._brushSize // 2
|
radius = self._brushSize // 2
|
||||||
|
|
||||||
# Draw the line itself
|
if self._brushShape == "rectangular":
|
||||||
draw.line([(img_x1, img_y1), (img_x2, img_y2)], fill=rgb_color, width=self._brushSize)
|
# For rectangular brush, draw multiple rectangles along the line
|
||||||
|
# Calculate the distance and number of steps
|
||||||
# Draw rounded ends (circles at start and end)
|
dx = img_x2 - img_x1
|
||||||
draw.ellipse([img_x1 - radius, img_y1 - radius, img_x1 + radius, img_y1 + radius],
|
dy = img_y2 - img_y1
|
||||||
fill=rgb_color)
|
distance = ((dx ** 2 + dy ** 2) ** 0.5)
|
||||||
draw.ellipse([img_x2 - radius, img_y2 - radius, img_x2 + radius, img_y2 + radius],
|
|
||||||
fill=rgb_color)
|
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
|
# Update the image
|
||||||
current_image._imageData = pil_image
|
current_image._imageData = pil_image
|
||||||
|
|||||||
Reference in New Issue
Block a user