714 lines
26 KiB
Python
714 lines
26 KiB
Python
"""
|
|||
|
|
Python 图形编辑器
|
||
|
|
类似简化版 Photoshop 的绘图软件
|
||
|
|
"""
|
||
|
|
|
||
|
|
import tkinter as tk
|
||
|
|
from tkinter import ttk, colorchooser, messagebox, filedialog
|
||
|
|
from PIL import Image, ImageDraw, ImageFilter, ImageEnhance, ImageTk
|
||
|
|
import numpy as np
|
||
|
|
import json
|
||
|
|
from typing import List, Tuple, Optional
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from enum import Enum
|
||
|
|
|
||
|
|
|
||
|
|
class ToolType(Enum):
|
||
|
|
"""工具类型"""
|
||
|
|
BRUSH = "brush"
|
||
|
|
PENCIL = "pencil"
|
||
|
|
ERASER = "eraser"
|
||
|
|
LINE = "line"
|
||
|
|
RECTANGLE = "rectangle"
|
||
|
|
ELLIPSE = "ellipse"
|
||
|
|
FILL = "fill"
|
||
|
|
PICKER = "picker"
|
||
|
|
TEXT = "text"
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Layer:
|
||
|
|
"""图层类"""
|
||
|
|
id: int
|
||
|
|
name: str
|
||
|
|
image: Image.Image
|
||
|
|
visible: bool = True
|
||
|
|
opacity: float = 1.0
|
||
|
|
|
||
|
|
|
||
|
|
class GraphicEditor:
|
||
|
|
"""图形编辑器主类"""
|
||
|
|
|
||
|
|
def __init__(self, root: tk.Tk):
|
||
|
|
"""
|
||
|
|
初始化图形编辑器
|
||
|
|
|
||
|
|
Args:
|
||
|
|
root: Tkinter 根窗口
|
||
|
|
"""
|
||
|
|
self.root = root
|
||
|
|
self.root.title("Python 图形编辑器")
|
||
|
|
self.root.geometry("1400x900")
|
||
|
|
|
||
|
|
# 图像参数
|
||
|
|
self.canvas_width = 800
|
||
|
|
self.canvas_height = 600
|
||
|
|
self.current_image: Optional[Image.Image] = None
|
||
|
|
self.draw_image: Optional[Image.Image] = None
|
||
|
|
self.draw_context: Optional[ImageDraw.ImageDraw] = None
|
||
|
|
|
||
|
|
# 工具参数
|
||
|
|
self.current_tool = ToolType.BRUSH
|
||
|
|
self.brush_size = 10
|
||
|
|
self.brush_color = "#000000"
|
||
|
|
self.eraser_size = 20
|
||
|
|
|
||
|
|
# 绘图状态
|
||
|
|
self.start_x = 0
|
||
|
|
self.start_y = 0
|
||
|
|
self.is_drawing = False
|
||
|
|
self.current_shape = None
|
||
|
|
|
||
|
|
# 图层管理
|
||
|
|
self.layers: List[Layer] = []
|
||
|
|
self.active_layer_id = 0
|
||
|
|
self.layer_counter = 0
|
||
|
|
|
||
|
|
# 历史记录(撤销/重做)
|
||
|
|
self.history: List[Image.Image] = []
|
||
|
|
self.history_index = -1
|
||
|
|
self.max_history = 20
|
||
|
|
|
||
|
|
# 历史记录
|
||
|
|
self.undo_stack = []
|
||
|
|
self.redo_stack = []
|
||
|
|
|
||
|
|
# 创建界面
|
||
|
|
self._create_ui()
|
||
|
|
|
||
|
|
# 初始化画布
|
||
|
|
self._init_canvas()
|
||
|
|
|
||
|
|
# 初始化图层
|
||
|
|
self._create_new_layer("背景")
|
||
|
|
|
||
|
|
def _create_ui(self):
|
||
|
|
"""创建用户界面"""
|
||
|
|
# 主框架
|
||
|
|
main_frame = ttk.Frame(self.root)
|
||
|
|
main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||
|
|
|
||
|
|
# 左侧工具栏
|
||
|
|
self._create_toolbar(main_frame)
|
||
|
|
|
||
|
|
# 中间画布区域
|
||
|
|
self._create_canvas_area(main_frame)
|
||
|
|
|
||
|
|
# 右侧面板
|
||
|
|
self._create_right_panel(main_frame)
|
||
|
|
|
||
|
|
# 底部状态栏
|
||
|
|
self._create_status_bar()
|
||
|
|
|
||
|
|
def _create_toolbar(self, parent):
|
||
|
|
"""创建工具栏"""
|
||
|
|
toolbar = ttk.Frame(parent, width=80)
|
||
|
|
toolbar.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 5))
|
||
|
|
|
||
|
|
# 工具选择
|
||
|
|
tools_frame = ttk.LabelFrame(toolbar, text="工具", padding=5)
|
||
|
|
tools_frame.pack(fill=tk.X, pady=(0, 10))
|
||
|
|
|
||
|
|
# 画笔工具
|
||
|
|
self.brush_btn = ttk.Button(tools_frame, text="🖌️ 画笔",
|
||
|
|
command=lambda: self._set_tool(ToolType.BRUSH))
|
||
|
|
self.brush_btn.pack(fill=tk.X, pady=2)
|
||
|
|
|
||
|
|
# 铅笔工具
|
||
|
|
self.pencil_btn = ttk.Button(tools_frame, text="✏️ 铅笔",
|
||
|
|
command=lambda: self._set_tool(ToolType.PENCIL))
|
||
|
|
self.pencil_btn.pack(fill=tk.X, pady=2)
|
||
|
|
|
||
|
|
# 橡皮擦
|
||
|
|
self.eraser_btn = ttk.Button(tools_frame, text="🧹 橡皮擦",
|
||
|
|
command=lambda: self._set_tool(ToolType.ERASER))
|
||
|
|
self.eraser_btn.pack(fill=tk.X, pady=2)
|
||
|
|
|
||
|
|
# 直线工具
|
||
|
|
self.line_btn = ttk.Button(tools_frame, text="📏 直线",
|
||
|
|
command=lambda: self._set_tool(ToolType.LINE))
|
||
|
|
self.line_btn.pack(fill=tk.X, pady=2)
|
||
|
|
|
||
|
|
# 矩形工具
|
||
|
|
self.rect_btn = ttk.Button(tools_frame, text="⬜ 矩形",
|
||
|
|
command=lambda: self._set_tool(ToolType.RECTANGLE))
|
||
|
|
self.rect_btn.pack(fill=tk.X, pady=2)
|
||
|
|
|
||
|
|
# 椭圆工具
|
||
|
|
self.ellipse_btn = ttk.Button(tools_frame, text="⭕ 椭圆",
|
||
|
|
command=lambda: self._set_tool(ToolType.ELLIPSE))
|
||
|
|
self.ellipse_btn.pack(fill=tk.X, pady=2)
|
||
|
|
|
||
|
|
# 填充工具
|
||
|
|
self.fill_btn = ttk.Button(tools_frame, text="🪣 填充",
|
||
|
|
command=lambda: self._set_tool(ToolType.FILL))
|
||
|
|
self.fill_btn.pack(fill=tk.X, pady=2)
|
||
|
|
|
||
|
|
# 取色器
|
||
|
|
self.picker_btn = ttk.Button(tools_frame, text="💉 取色器",
|
||
|
|
command=lambda: self._set_tool(ToolType.PICKER))
|
||
|
|
self.picker_btn.pack(fill=tk.X, pady=2)
|
||
|
|
|
||
|
|
# 颜色选择
|
||
|
|
color_frame = ttk.LabelFrame(toolbar, text="颜色", padding=5)
|
||
|
|
color_frame.pack(fill=tk.X, pady=(0, 10))
|
||
|
|
|
||
|
|
self.color_btn = tk.Button(color_frame, bg=self.brush_color,
|
||
|
|
command=self._choose_color, height=2)
|
||
|
|
self.color_btn.pack(fill=tk.X, pady=2)
|
||
|
|
|
||
|
|
# 画笔大小
|
||
|
|
size_frame = ttk.LabelFrame(toolbar, text="画笔大小", padding=5)
|
||
|
|
size_frame.pack(fill=tk.X, pady=(0, 10))
|
||
|
|
|
||
|
|
self.size_slider = ttk.Scale(size_frame, from_=1, to=100,
|
||
|
|
orient=tk.HORIZONTAL,
|
||
|
|
command=self._update_brush_size)
|
||
|
|
self.size_slider.set(self.brush_size)
|
||
|
|
self.size_slider.pack(fill=tk.X)
|
||
|
|
|
||
|
|
self.size_label = ttk.Label(size_frame, text=str(self.brush_size))
|
||
|
|
self.size_label.pack()
|
||
|
|
|
||
|
|
# 操作按钮
|
||
|
|
ops_frame = ttk.LabelFrame(toolbar, text="操作", padding=5)
|
||
|
|
ops_frame.pack(fill=tk.X, pady=(0, 10))
|
||
|
|
|
||
|
|
ttk.Button(ops_frame, text="↩️ 撤销", command=self._undo).pack(fill=tk.X, pady=2)
|
||
|
|
ttk.Button(ops_frame, text="↪️ 重做", command=self._redo).pack(fill=tk.X, pady=2)
|
||
|
|
ttk.Button(ops_frame, text="🗑️ 清除", command=self._clear_canvas).pack(fill=tk.X, pady=2)
|
||
|
|
|
||
|
|
def _create_canvas_area(self, parent):
|
||
|
|
"""创建画布区域"""
|
||
|
|
canvas_frame = ttk.Frame(parent)
|
||
|
|
canvas_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
|
|
|
||
|
|
# 画布容器
|
||
|
|
self.canvas_container = tk.Canvas(canvas_frame,
|
||
|
|
width=self.canvas_width,
|
||
|
|
height=self.canvas_height,
|
||
|
|
bg='gray')
|
||
|
|
self.canvas_container.pack(padx=10, pady=10)
|
||
|
|
|
||
|
|
# 创建 Tkinter 画布
|
||
|
|
self.canvas = tk.Canvas(self.canvas_container,
|
||
|
|
width=self.canvas_width,
|
||
|
|
height=self.canvas_height,
|
||
|
|
bg='white',
|
||
|
|
cursor='crosshair')
|
||
|
|
self.canvas_container.create_window(
|
||
|
|
self.canvas_width // 2,
|
||
|
|
self.canvas_height // 2,
|
||
|
|
window=self.canvas
|
||
|
|
)
|
||
|
|
|
||
|
|
# 绑定事件
|
||
|
|
self.canvas.bind("<Button-1>", self._on_mouse_down)
|
||
|
|
self.canvas.bind("<B1-Motion>", self._on_mouse_drag)
|
||
|
|
self.canvas.bind("<ButtonRelease-1>", self._on_mouse_up)
|
||
|
|
|
||
|
|
def _create_right_panel(self, parent):
|
||
|
|
"""创建右侧面板"""
|
||
|
|
right_panel = ttk.Frame(parent, width=250)
|
||
|
|
right_panel.pack(side=tk.RIGHT, fill=tk.Y, padx=(5, 0))
|
||
|
|
|
||
|
|
# 图层面板
|
||
|
|
layers_frame = ttk.LabelFrame(right_panel, text="图层", padding=5)
|
||
|
|
layers_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
|
||
|
|
|
||
|
|
# 图层列表
|
||
|
|
self.layers_listbox = tk.Listbox(layers_frame, height=10)
|
||
|
|
self.layers_listbox.pack(fill=tk.X, pady=2)
|
||
|
|
self.layers_listbox.bind('<<ListboxSelect>>', self._on_layer_select)
|
||
|
|
|
||
|
|
# 图层操作按钮
|
||
|
|
layers_btn_frame = ttk.Frame(layers_frame)
|
||
|
|
layers_btn_frame.pack(fill=tk.X, pady=5)
|
||
|
|
|
||
|
|
ttk.Button(layers_btn_frame, text="+",
|
||
|
|
command=self._create_new_layer).pack(side=tk.LEFT, padx=2)
|
||
|
|
ttk.Button(layers_btn_frame, text="-",
|
||
|
|
command=self._delete_layer).pack(side=tk.LEFT, padx=2)
|
||
|
|
ttk.Button(layers_btn_frame, text="👁️",
|
||
|
|
command=self._toggle_layer_visibility).pack(side=tk.LEFT, padx=2)
|
||
|
|
ttk.Button(layers_btn_frame, text="⬆️",
|
||
|
|
command=self._move_layer_up).pack(side=tk.LEFT, padx=2)
|
||
|
|
ttk.Button(layers_btn_frame, text="⬇️",
|
||
|
|
command=self._move_layer_down).pack(side=tk.LEFT, padx=2)
|
||
|
|
|
||
|
|
# 滤镜面板
|
||
|
|
filters_frame = ttk.LabelFrame(right_panel, text="滤镜", padding=5)
|
||
|
|
filters_frame.pack(fill=tk.X, pady=(0, 10))
|
||
|
|
|
||
|
|
ttk.Button(filters_frame, text="🌫️ 高斯模糊",
|
||
|
|
command=lambda: self._apply_filter("blur")).pack(fill=tk.X, pady=2)
|
||
|
|
ttk.Button(filters_frame, text="🔍 锐化",
|
||
|
|
command=lambda: self._apply_filter("sharpen")).pack(fill=tk.X, pady=2)
|
||
|
|
ttk.Button(filters_frame, text="🌓 边缘检测",
|
||
|
|
command=lambda: self._apply_filter("edge")).pack(fill=tk.X, pady=2)
|
||
|
|
ttk.Button(filters_frame, text="🎨 浮雕",
|
||
|
|
command=lambda: self._apply_filter("emboss")).pack(fill=tk.X, pady=2)
|
||
|
|
|
||
|
|
# 调整面板
|
||
|
|
adjust_frame = ttk.LabelFrame(right_panel, text="图像调整", padding=5)
|
||
|
|
adjust_frame.pack(fill=tk.X)
|
||
|
|
|
||
|
|
ttk.Button(adjust_frame, text="☀️ 亮度",
|
||
|
|
command=lambda: self._adjust_image("brightness")).pack(fill=tk.X, pady=2)
|
||
|
|
ttk.Button(adjust_frame, text="🎭 对比度",
|
||
|
|
command=lambda: self._adjust_image("contrast")).pack(fill=tk.X, pady=2)
|
||
|
|
ttk.Button(adjust_frame, text="🎨 饱和度",
|
||
|
|
command=lambda: self._adjust_image("color")).pack(fill=tk.X, pady=2)
|
||
|
|
|
||
|
|
# 文件操作
|
||
|
|
file_frame = ttk.LabelFrame(right_panel, text="文件", padding=5)
|
||
|
|
file_frame.pack(fill=tk.X, pady=(10, 0))
|
||
|
|
|
||
|
|
ttk.Button(file_frame, text="📂 新建",
|
||
|
|
command=self._new_file).pack(fill=tk.X, pady=2)
|
||
|
|
ttk.Button(file_frame, text="📂 打开",
|
||
|
|
command=self._open_file).pack(fill=tk.X, pady=2)
|
||
|
|
ttk.Button(file_frame, text="💾 保存",
|
||
|
|
command=self._save_file).pack(fill=tk.X, pady=2)
|
||
|
|
ttk.Button(file_frame, text="💾 另存为",
|
||
|
|
command=self._save_file_as).pack(fill=tk.X, pady=2)
|
||
|
|
ttk.Button(file_frame, text="📤 导出",
|
||
|
|
command=self._export_file).pack(fill=tk.X, pady=2)
|
||
|
|
|
||
|
|
def _create_status_bar(self):
|
||
|
|
"""创建状态栏"""
|
||
|
|
self.status_bar = ttk.Label(self.root, text="就绪", relief=tk.SUNKEN)
|
||
|
|
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
|
||
|
|
|
||
|
|
def _init_canvas(self):
|
||
|
|
"""初始化画布"""
|
||
|
|
self.current_image = Image.new('RGB',
|
||
|
|
(self.canvas_width, self.canvas_height),
|
||
|
|
'white')
|
||
|
|
self._update_canvas_display()
|
||
|
|
|
||
|
|
def _update_canvas_display(self):
|
||
|
|
"""更新画布显示"""
|
||
|
|
if self.current_image:
|
||
|
|
self.tk_image = ImageTk.PhotoImage(self.current_image)
|
||
|
|
self.canvas.create_image(0, 0, anchor=tk.NW, image=self.tk_image)
|
||
|
|
|
||
|
|
def _create_new_layer(self, name: str = None):
|
||
|
|
"""创建新图层"""
|
||
|
|
if name is None:
|
||
|
|
name = f"图层 {self.layer_counter + 1}"
|
||
|
|
|
||
|
|
self.layer_counter += 1
|
||
|
|
layer = Layer(
|
||
|
|
id=self.layer_counter,
|
||
|
|
name=name,
|
||
|
|
image=Image.new('RGBA', (self.canvas_width, self.canvas_height), (0, 0, 0, 0))
|
||
|
|
)
|
||
|
|
self.layers.append(layer)
|
||
|
|
|
||
|
|
if not self.active_layer_id:
|
||
|
|
self.active_layer_id = layer.id
|
||
|
|
|
||
|
|
self._update_layers_listbox()
|
||
|
|
self._status(f"创建新图层:{name}")
|
||
|
|
|
||
|
|
def _update_layers_listbox(self):
|
||
|
|
"""更新图层列表"""
|
||
|
|
self.layers_listbox.delete(0, tk.END)
|
||
|
|
for layer in reversed(self.layers):
|
||
|
|
visibility = "👁️" if layer.visible else "❌"
|
||
|
|
marker = "▶️" if layer.id == self.active_layer_id else ""
|
||
|
|
self.layers_listbox.insert(tk.END, f"{marker} {visibility} {layer.name}")
|
||
|
|
|
||
|
|
def _set_tool(self, tool: ToolType):
|
||
|
|
"""设置当前工具"""
|
||
|
|
self.current_tool = tool
|
||
|
|
self._status(f"当前工具:{tool.value}")
|
||
|
|
|
||
|
|
def _update_brush_size(self, value):
|
||
|
|
"""更新画笔大小"""
|
||
|
|
self.brush_size = int(float(value))
|
||
|
|
self.size_label.config(text=str(self.brush_size))
|
||
|
|
|
||
|
|
def _choose_color(self):
|
||
|
|
"""选择颜色"""
|
||
|
|
color = colorchooser.askcolor(title="选择颜色")[1]
|
||
|
|
if color:
|
||
|
|
self.brush_color = color
|
||
|
|
self.color_btn.config(bg=color)
|
||
|
|
self._status(f"选择颜色:{color}")
|
||
|
|
|
||
|
|
def _on_mouse_down(self, event):
|
||
|
|
"""鼠标按下事件"""
|
||
|
|
self.is_drawing = True
|
||
|
|
self.start_x = event.x
|
||
|
|
self.start_y = event.y
|
||
|
|
|
||
|
|
# 保存当前状态用于撤销
|
||
|
|
self._save_history()
|
||
|
|
|
||
|
|
if self.current_tool in [ToolType.BRUSH, ToolType.PENCIL, ToolType.ERASER]:
|
||
|
|
self._draw_point(event.x, event.y)
|
||
|
|
|
||
|
|
def _on_mouse_drag(self, event):
|
||
|
|
"""鼠标拖动事件"""
|
||
|
|
if not self.is_drawing:
|
||
|
|
return
|
||
|
|
|
||
|
|
if self.current_tool in [ToolType.BRUSH, ToolType.PENCIL, ToolType.ERASER]:
|
||
|
|
self._draw_line(self.start_x, self.start_y, event.x, event.y)
|
||
|
|
elif self.current_tool in [ToolType.LINE, ToolType.RECTANGLE, ToolType.ELLIPSE]:
|
||
|
|
self._draw_shape(event.x, event.y)
|
||
|
|
|
||
|
|
self.start_x = event.x
|
||
|
|
self.start_y = event.y
|
||
|
|
|
||
|
|
def _on_mouse_up(self, event):
|
||
|
|
"""鼠标释放事件"""
|
||
|
|
self.is_drawing = False
|
||
|
|
|
||
|
|
if self.current_tool == ToolType.FILL:
|
||
|
|
self._flood_fill(event.x, event.y)
|
||
|
|
elif self.current_tool == ToolType.PICKER:
|
||
|
|
self._pick_color(event.x, event.y)
|
||
|
|
|
||
|
|
def _draw_point(self, x: int, y: int):
|
||
|
|
"""绘制点"""
|
||
|
|
if self.current_tool == ToolType.ERASER:
|
||
|
|
color = (255, 255, 255, 255) if self.draw_image.mode == 'RGBA' else (255, 255, 255)
|
||
|
|
else:
|
||
|
|
color = self._hex_to_rgb(self.brush_color)
|
||
|
|
|
||
|
|
radius = self.brush_size if self.current_tool != ToolType.ERASER else self.eraser_size
|
||
|
|
|
||
|
|
if self.draw_context:
|
||
|
|
if self.current_tool == ToolType.PENCIL:
|
||
|
|
self.draw_context.point((x, y), fill=color)
|
||
|
|
else:
|
||
|
|
self.draw_context.ellipse(
|
||
|
|
[x - radius//2, y - radius//2, x + radius//2, y + radius//2],
|
||
|
|
fill=color
|
||
|
|
)
|
||
|
|
|
||
|
|
self._update_canvas_display()
|
||
|
|
|
||
|
|
def _draw_line(self, x1: int, y1: int, x2: int, y2: int):
|
||
|
|
"""绘制线段"""
|
||
|
|
if self.current_tool == ToolType.ERASER:
|
||
|
|
color = (255, 255, 255, 255) if self.draw_image.mode == 'RGBA' else (255, 255, 255)
|
||
|
|
else:
|
||
|
|
color = self._hex_to_rgb(self.brush_color)
|
||
|
|
|
||
|
|
width = self.brush_size if self.current_tool != ToolType.ERASER else self.eraser_size
|
||
|
|
|
||
|
|
if self.draw_context:
|
||
|
|
self.draw_context.line([x1, y1, x2, y2], fill=color, width=width)
|
||
|
|
|
||
|
|
self._update_canvas_display()
|
||
|
|
|
||
|
|
def _draw_shape(self, x: int, y: int):
|
||
|
|
"""绘制形状"""
|
||
|
|
# 清除之前的预览
|
||
|
|
self._update_canvas_display()
|
||
|
|
|
||
|
|
color = self._hex_to_rgb(self.brush_color)
|
||
|
|
width = self.brush_size
|
||
|
|
|
||
|
|
if self.draw_context:
|
||
|
|
if self.current_tool == ToolType.LINE:
|
||
|
|
self.draw_context.line([self.start_x, self.start_y, x, y], fill=color, width=width)
|
||
|
|
elif self.current_tool == ToolType.RECTANGLE:
|
||
|
|
self.draw_context.rectangle([self.start_x, self.start_y, x, y], outline=color, width=width)
|
||
|
|
elif self.current_tool == ToolType.ELLIPSE:
|
||
|
|
self.draw_context.ellipse([self.start_x, self.start_y, x, y], outline=color, width=width)
|
||
|
|
|
||
|
|
self._update_canvas_display()
|
||
|
|
|
||
|
|
def _flood_fill(self, x: int, y: int):
|
||
|
|
"""洪水填充算法"""
|
||
|
|
if not self.current_image:
|
||
|
|
return
|
||
|
|
|
||
|
|
# 转换为 numpy 数组
|
||
|
|
img_array = np.array(self.current_image)
|
||
|
|
|
||
|
|
# 获取目标颜色
|
||
|
|
target_color = img_array[y, x]
|
||
|
|
fill_color = np.array(self._hex_to_rgb(self.brush_color) + [255])
|
||
|
|
|
||
|
|
# 如果颜色相同则不填充
|
||
|
|
if np.array_equal(target_color[:3], fill_color[:3]):
|
||
|
|
return
|
||
|
|
|
||
|
|
# BFS 填充
|
||
|
|
height, width = img_array.shape[:2]
|
||
|
|
visited = np.zeros((height, width), dtype=bool)
|
||
|
|
stack = [(x, y)]
|
||
|
|
|
||
|
|
while stack:
|
||
|
|
cx, cy = stack.pop()
|
||
|
|
|
||
|
|
if cx < 0 or cx >= width or cy < 0 or cy >= height:
|
||
|
|
continue
|
||
|
|
|
||
|
|
if visited[cy, cx]:
|
||
|
|
continue
|
||
|
|
|
||
|
|
current_color = img_array[cy, cx]
|
||
|
|
|
||
|
|
# 检查颜色是否相似
|
||
|
|
if np.linalg.norm(current_color[:3] - target_color[:3]) > 30:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# 设置新颜色
|
||
|
|
img_array[cy, cx] = fill_color
|
||
|
|
visited[cy, cx] = True
|
||
|
|
|
||
|
|
# 添加相邻像素
|
||
|
|
stack.extend([(cx+1, cy), (cx-1, cy), (cx, cy+1), (cx, cy-1)])
|
||
|
|
|
||
|
|
# 更新图像
|
||
|
|
self.current_image = Image.fromarray(img_array)
|
||
|
|
self._update_canvas_display()
|
||
|
|
self._status("填充完成")
|
||
|
|
|
||
|
|
def _pick_color(self, x: int, y: int):
|
||
|
|
"""取色"""
|
||
|
|
if not self.current_image:
|
||
|
|
return
|
||
|
|
|
||
|
|
pixel = self.current_image.getpixel((x, y))
|
||
|
|
if len(pixel) == 4:
|
||
|
|
pixel = pixel[:3]
|
||
|
|
|
||
|
|
color = '#%02x%02x%02x' % pixel
|
||
|
|
self.brush_color = color
|
||
|
|
self.color_btn.config(bg=color)
|
||
|
|
self._status(f"取色:{color}")
|
||
|
|
|
||
|
|
def _apply_filter(self, filter_type: str):
|
||
|
|
"""应用滤镜"""
|
||
|
|
if not self.current_image:
|
||
|
|
return
|
||
|
|
|
||
|
|
self._save_history()
|
||
|
|
|
||
|
|
if filter_type == "blur":
|
||
|
|
self.current_image = self.current_image.filter(ImageFilter.GaussianBlur(radius=3))
|
||
|
|
elif filter_type == "sharpen":
|
||
|
|
self.current_image = self.current_image.filter(ImageFilter.SHARPEN)
|
||
|
|
elif filter_type == "edge":
|
||
|
|
self.current_image = self.current_image.filter(ImageFilter.FIND_EDGES)
|
||
|
|
elif filter_type == "emboss":
|
||
|
|
self.current_image = self.current_image.filter(ImageFilter.EMBOSS)
|
||
|
|
|
||
|
|
self._update_canvas_display()
|
||
|
|
self._status(f"应用滤镜:{filter_type}")
|
||
|
|
|
||
|
|
def _adjust_image(self, adjust_type: str):
|
||
|
|
"""调整图像"""
|
||
|
|
if not self.current_image:
|
||
|
|
return
|
||
|
|
|
||
|
|
self._save_history()
|
||
|
|
|
||
|
|
if adjust_type == "brightness":
|
||
|
|
enhancer = ImageEnhance.Brightness(self.current_image)
|
||
|
|
self.current_image = enhancer.enhance(1.2)
|
||
|
|
elif adjust_type == "contrast":
|
||
|
|
enhancer = ImageEnhance.Contrast(self.current_image)
|
||
|
|
self.current_image = enhancer.enhance(1.2)
|
||
|
|
elif adjust_type == "color":
|
||
|
|
enhancer = ImageEnhance.Color(self.current_image)
|
||
|
|
self.current_image = enhancer.enhance(1.2)
|
||
|
|
|
||
|
|
self._update_canvas_display()
|
||
|
|
self._status(f"调整:{adjust_type}")
|
||
|
|
|
||
|
|
def _save_history(self):
|
||
|
|
"""保存历史状态"""
|
||
|
|
if self.current_image:
|
||
|
|
self.undo_stack.append(self.current_image.copy())
|
||
|
|
if len(self.undo_stack) > self.max_history:
|
||
|
|
self.undo_stack.pop(0)
|
||
|
|
self.redo_stack.clear()
|
||
|
|
|
||
|
|
def _undo(self):
|
||
|
|
"""撤销"""
|
||
|
|
if self.undo_stack:
|
||
|
|
self.redo_stack.append(self.current_image.copy())
|
||
|
|
self.current_image = self.undo_stack.pop()
|
||
|
|
self._update_canvas_display()
|
||
|
|
self._status("撤销")
|
||
|
|
|
||
|
|
def _redo(self):
|
||
|
|
"""重做"""
|
||
|
|
if self.redo_stack:
|
||
|
|
self.undo_stack.append(self.current_image.copy())
|
||
|
|
self.current_image = self.redo_stack.pop()
|
||
|
|
self._update_canvas_display()
|
||
|
|
self._status("重做")
|
||
|
|
|
||
|
|
def _clear_canvas(self):
|
||
|
|
"""清除画布"""
|
||
|
|
self._save_history()
|
||
|
|
self.current_image = Image.new('RGB', (self.canvas_width, self.canvas_height), 'white')
|
||
|
|
self._update_canvas_display()
|
||
|
|
self._status("画布已清除")
|
||
|
|
|
||
|
|
def _on_layer_select(self, event):
|
||
|
|
"""图层选择事件"""
|
||
|
|
selection = self.layers_listbox.curselection()
|
||
|
|
if selection:
|
||
|
|
index = len(self.layers) - 1 - selection[0]
|
||
|
|
if 0 <= index < len(self.layers):
|
||
|
|
self.active_layer_id = self.layers[index].id
|
||
|
|
self._update_layers_listbox()
|
||
|
|
|
||
|
|
def _toggle_layer_visibility(self):
|
||
|
|
"""切换图层可见性"""
|
||
|
|
selection = self.layers_listbox.curselection()
|
||
|
|
if selection:
|
||
|
|
index = len(self.layers) - 1 - selection[0]
|
||
|
|
if 0 <= index < len(self.layers):
|
||
|
|
self.layers[index].visible = not self.layers[index].visible
|
||
|
|
self._update_layers_listbox()
|
||
|
|
self._status(f"图层 {self.layers[index].name} {'可见' if self.layers[index].visible else '隐藏'}")
|
||
|
|
|
||
|
|
def _move_layer_up(self):
|
||
|
|
"""上移图层"""
|
||
|
|
selection = self.layers_listbox.curselection()
|
||
|
|
if selection and selection[0] > 0:
|
||
|
|
index = selection[0]
|
||
|
|
self.layers.reverse()
|
||
|
|
self.layers[index], self.layers[index-1] = self.layers[index-1], self.layers[index]
|
||
|
|
self.layers.reverse()
|
||
|
|
self._update_layers_listbox()
|
||
|
|
|
||
|
|
def _move_layer_down(self):
|
||
|
|
"""下移图层"""
|
||
|
|
selection = self.layers_listbox.curselection()
|
||
|
|
if selection and selection[0] < len(self.layers) - 1:
|
||
|
|
index = selection[0]
|
||
|
|
self.layers.reverse()
|
||
|
|
self.layers[index], self.layers[index+1] = self.layers[index+1], self.layers[index]
|
||
|
|
self.layers.reverse()
|
||
|
|
self._update_layers_listbox()
|
||
|
|
|
||
|
|
def _delete_layer(self):
|
||
|
|
"""删除图层"""
|
||
|
|
selection = self.layers_listbox.curselection()
|
||
|
|
if selection and len(self.layers) > 1:
|
||
|
|
index = len(self.layers) - 1 - selection[0]
|
||
|
|
deleted = self.layers.pop(index)
|
||
|
|
self._update_layers_listbox()
|
||
|
|
self._status(f"删除图层:{deleted.name}")
|
||
|
|
|
||
|
|
def _new_file(self):
|
||
|
|
"""新建文件"""
|
||
|
|
if messagebox.askyesno("新建", "确定要新建文件吗?未保存的内容将丢失。"):
|
||
|
|
self._save_history()
|
||
|
|
self.current_image = Image.new('RGB', (self.canvas_width, self.canvas_height), 'white')
|
||
|
|
self._update_canvas_display()
|
||
|
|
self._status("新建文件")
|
||
|
|
|
||
|
|
def _open_file(self):
|
||
|
|
"""打开文件"""
|
||
|
|
file_path = filedialog.askopenfilename(
|
||
|
|
title="打开文件",
|
||
|
|
filetypes=[
|
||
|
|
("图像文件", "*.png *.jpg *.jpeg *.bmp *.gif"),
|
||
|
|
("所有文件", "*.*")
|
||
|
|
]
|
||
|
|
)
|
||
|
|
|
||
|
|
if file_path:
|
||
|
|
try:
|
||
|
|
self._save_history()
|
||
|
|
img = Image.open(file_path)
|
||
|
|
img = img.convert('RGBA')
|
||
|
|
img = img.resize((self.canvas_width, self.canvas_height))
|
||
|
|
self.current_image = img
|
||
|
|
self._update_canvas_display()
|
||
|
|
self._status(f"打开文件:{file_path}")
|
||
|
|
except Exception as e:
|
||
|
|
messagebox.showerror("错误", f"无法打开文件:{str(e)}")
|
||
|
|
|
||
|
|
def _save_file(self):
|
||
|
|
"""保存文件"""
|
||
|
|
self._save_file_as()
|
||
|
|
|
||
|
|
def _save_file_as(self):
|
||
|
|
"""另存为"""
|
||
|
|
file_path = filedialog.asksaveasfilename(
|
||
|
|
title="保存文件",
|
||
|
|
defaultextension=".png",
|
||
|
|
filetypes=[
|
||
|
|
("PNG 文件", "*.png"),
|
||
|
|
("JPEG 文件", "*.jpg"),
|
||
|
|
("BMP 文件", "*.bmp"),
|
||
|
|
("所有文件", "*.*")
|
||
|
|
]
|
||
|
|
)
|
||
|
|
|
||
|
|
if file_path:
|
||
|
|
try:
|
||
|
|
self.current_image.save(file_path)
|
||
|
|
self._status(f"文件已保存:{file_path}")
|
||
|
|
except Exception as e:
|
||
|
|
messagebox.showerror("错误", f"无法保存文件:{str(e)}")
|
||
|
|
|
||
|
|
def _export_file(self):
|
||
|
|
"""导出文件"""
|
||
|
|
file_path = filedialog.asksaveasfilename(
|
||
|
|
title="导出文件",
|
||
|
|
defaultextension=".png",
|
||
|
|
filetypes=[
|
||
|
|
("PNG 文件", "*.png"),
|
||
|
|
("JPEG 文件", "*.jpg"),
|
||
|
|
("PDF 文件", "*.pdf"),
|
||
|
|
("所有文件", "*.*")
|
||
|
|
]
|
||
|
|
)
|
||
|
|
|
||
|
|
if file_path:
|
||
|
|
try:
|
||
|
|
self.current_image.save(file_path)
|
||
|
|
self._status(f"文件已导出:{file_path}")
|
||
|
|
except Exception as e:
|
||
|
|
messagebox.showerror("错误", f"无法导出文件:{str(e)}")
|
||
|
|
|
||
|
|
def _hex_to_rgb(self, hex_color: str) -> tuple:
|
||
|
|
"""十六进制颜色转 RGB"""
|
||
|
|
hex_color = hex_color.lstrip('#')
|
||
|
|
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
|
||
|
|
|
||
|
|
def _status(self, message: str):
|
||
|
|
"""更新状态栏"""
|
||
|
|
self.status_bar.config(text=message)
|
||
|
|
|
||
|
|
def run(self):
|
||
|
|
"""运行应用程序"""
|
||
|
|
self.root.mainloop()
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
"""主函数"""
|
||
|
|
root = tk.Tk()
|
||
|
|
editor = GraphicEditor(root)
|
||
|
|
editor.run()
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|