diff --git a/GIFCraft.py b/GIFCraft.py index afc8037..1a28902 100644 --- a/GIFCraft.py +++ b/GIFCraft.py @@ -1,8 +1,9 @@ import tkinter as tk -from tkinter import filedialog, messagebox, Menu, Checkbutton, IntVar, Scrollbar, simpledialog +from tkinter import filedialog, messagebox, Frame, Canvas, Menu, Checkbutton, IntVar, Scrollbar, simpledialog from PIL import Image, ImageTk, ImageSequence import os + class GIFEditor: def __init__(self, master): """Initialize the GIF editor with the main window and UI setup.""" @@ -10,6 +11,7 @@ class GIFEditor: self.master.title("GIFCraft - GIF Editor") self.master.geometry("800x600") + # Initial settings self.frame_index = 0 self.frames = [] self.delays = [] @@ -20,6 +22,7 @@ class GIFEditor: self.checkbox_vars = [] self.check_all = tk.BooleanVar(value=False) + # Setup UI and bindings self.setup_ui() self.bind_keyboard_events() @@ -42,7 +45,6 @@ class GIFEditor: self.menu_bar = Menu(self.master) self.create_file_menu() self.create_edit_menu() - self.create_effects_menu() self.create_animation_menu() self.create_help_menu() self.master.config(menu=self.menu_bar) @@ -69,9 +71,17 @@ class GIFEditor: edit_menu.add_command(label="Add Empty Frame", command=self.add_empty_frame) edit_menu.add_command(label="Delete Frame(s)", command=self.delete_frames, accelerator="Del") edit_menu.add_separator() - edit_menu.add_command(label="Move to Position", command=self.move_frames_to_position) + edit_menu.add_command(label="Move Selected Frames", command=self.prompt_and_move_selected_frames) edit_menu.add_command(label="Move Frame Up", command=self.move_frame_up, accelerator="Arrow Up") edit_menu.add_command(label="Move Frame Down", command=self.move_frame_down, accelerator="Arrow Down") + edit_menu.add_separator() + edit_menu.add_command(label="Rotate Selected Frames 180º", command=self.rotate_selected_frames_180) + edit_menu.add_command(label="Rotate Selected Frames 90º CW", command=self.rotate_selected_frames_90_cw) + edit_menu.add_command(label="Rotate Selected Frames 90º CCW", command=self.rotate_selected_frames_90_ccw) + edit_menu.add_command(label="Rotate Selected Frames...", command=self.rotate_selected_frames) + edit_menu.add_separator() + edit_menu.add_command(label="Flip Selected Frames Horizontal", command=self.flip_selected_frames_horizontal) + edit_menu.add_command(label="Flip Selected Frames Vertical", command=self.flip_selected_frames_vertical) edit_menu.add_separator() edit_menu.add_command(label="Check/Uncheck All", command=self.toggle_check_all, accelerator="A") edit_menu.add_separator() @@ -82,16 +92,9 @@ class GIFEditor: edit_menu.add_command(label="Paste", command=self.paste_frames, accelerator="Ctrl+V") edit_menu.add_separator() edit_menu.add_command(label="Undo", command=self.undo, accelerator="Ctrl+Z") - edit_menu.add_command(label="Redo", command=self.redo, accelerator="Ctrl+Y") + edit_menu.add_command(label="Redo", command=self.redo, accelerator="Ctrl+Y") self.menu_bar.add_cascade(label="Edit", menu=edit_menu) - def create_effects_menu(self): - """Create the Effects menu.""" - effects_menu = Menu(self.menu_bar, tearoff=0) - effects_menu.add_command(label="Apply Crossfade Effect", command=self.apply_crossfade_effect) - effects_menu.add_command(label="Reverse Frames", command=self.reverse_frames) - self.menu_bar.add_cascade(label="Effects", menu=effects_menu) - def create_animation_menu(self): """Create the Animation menu.""" animation_menu = Menu(self.menu_bar, tearoff=0) @@ -104,19 +107,624 @@ class GIFEditor: help_menu.add_command(label="About", command=self.show_about) self.menu_bar.add_cascade(label="Help", menu=help_menu) + def new_file(self, event=None): + """Create a new file, prompting to save unsaved changes if any.""" + if self.frames: + response = messagebox.askyesnocancel("Unsaved Changes", "Do you want to save the current file before creating a new one?") + if response: # Yes + self.save() + if self.frames: # If saving was cancelled or failed, do not proceed + return + elif response is None: # Cancel + return + + # Reset the editor state for a new file + self.frames = [] + self.delays = [] + self.checkbox_vars = [] + self.current_file = None + self.frame_index = 0 + self.base_size = None # Clear the base size + self.update_frame_list() + self.show_frame() + self.update_title() + + def load_file(self, event=None): + """Load a GIF, PNG, or WebP file and extract its frames.""" + file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.gif *.png *.webp")]) + if not file_path: + return + + self.save_state() # Save the state before making changes + self.frames = [] + self.delays = [] + self.checkbox_vars = [] + for widget in self.frame_list.winfo_children(): + widget.destroy() + + try: + with Image.open(file_path) as img: + for i, frame in enumerate(ImageSequence.Iterator(img)): + if i == 0: + self.base_size = frame.size # Store the size of the first frame + self.frames.append(self.resize_to_base_size(frame.copy())) + delay = int(frame.info.get('duration', 100)) # Ensure delay is always an integer + self.delays.append(delay) + var = IntVar() + var.trace_add('write', lambda *args, i=len(self.checkbox_vars): self.set_current_frame(i)) + self.checkbox_vars.append(var) + self.frame_index = 0 + self.update_frame_list() + self.show_frame() + self.current_file = file_path + self.update_title() + except Exception as e: + messagebox.showerror("Error", f"Failed to load file: {e}") + + def save(self, event=None): + """Save the current frames and delays to a GIF file.""" + if self.current_file: + self.save_to_file(self.current_file) + else: + self.save_as() + + def save_as(self, event=None): + """Save the current frames and delays to a file with the selected format.""" + file_path = filedialog.asksaveasfilename(defaultextension=".gif", filetypes=[("GIF files", "*.gif"), ("PNG files", "*.png"), ("WebP files", "*.webp")]) + if file_path: + self.save_to_file(file_path) + self.current_file = file_path + self.update_title() + + def save_as_high_quality_gif(self): + """Save the current frames and delays to a high-quality GIF file using dithering.""" + file_path = filedialog.asksaveasfilename(defaultextension=".gif", filetypes=[("GIF files", "*.gif")]) + if file_path: + try: + images = [frame.convert("RGB").quantize(method=0) for frame in self.frames] + images[0].save(file_path, save_all=True, append_images=images[1:], duration=self.delays, loop=0, dither=Image.NONE) + self.current_file = file_path + self.update_title() + messagebox.showinfo("Success", "High-quality GIF saved successfully!") + except Exception as e: + messagebox.showerror("Error", f"Failed to save high-quality GIF: {e}") + + def extract_frames(self): + """Extract the frames and save them as individual images.""" + if not self.frames: + messagebox.showerror("Error", "No frames to extract.") + return + + folder_path = filedialog.askdirectory() + if not folder_path: + return + + try: + for i, frame in enumerate(self.frames): + frame_path = os.path.join(folder_path, f"frame_{i + 1}.png") + frame.save(frame_path) + messagebox.showinfo("Success", "Frames extracted successfully!") + except Exception as e: + messagebox.showerror("Error", f"Failed to extract frames: {e}") + + def save_to_file(self, file_path): + """Save the frames and delays to the specified file in the given format.""" + if self.frames: + try: + _, ext = os.path.splitext(file_path) + ext = ext[1:].lower() # Remove the dot and convert to lowercase + if ext == 'gif': + self.frames[0].save(file_path, save_all=True, append_images=self.frames[1:], duration=self.delays, loop=0) + elif ext == 'png': + self.frames[0].save(file_path, save_all=True, append_images=self.frames[1:], duration=self.delays, loop=0, format='PNG') + elif ext == 'webp': + self.frames[0].save(file_path, save_all=True, append_images=self.frames[1:], duration=self.delays, loop=0, format='WEBP') + else: + messagebox.showerror("Error", f"Unsupported file format: {ext.upper()}") + return + self.current_file = file_path + self.update_title() + messagebox.showinfo("Success", f"{ext.upper()} saved successfully!") + except Exception as e: + messagebox.showerror("Error", f"Failed to save {ext.upper()}: {e}") + + def add_image(self): + """Add images to the frames.""" + file_paths = filedialog.askopenfilenames(filetypes=[("Image files", "*.jpg *.jpeg *.png *.webp *.gif *.bmp")]) + if not file_paths: + return + + self.save_state() # Save the state before making changes + try: + for file_path in file_paths: + with Image.open(file_path) as image: + # Resize the new image to match the base dimensions + if not self.frames: # If no frames, set the base size to the first image's size + self.base_size = image.size + image = self.resize_to_base_size(image.copy()) + self.frames.append(image) + self.delays.append(100) # Default delay for added images + var = IntVar() + var.trace_add('write', lambda *args, i=len(self.checkbox_vars): self.set_current_frame(i)) + self.checkbox_vars.append(var) + + self.update_frame_list() + self.show_frame() + except Exception as e: + messagebox.showerror("Error", f"Failed to add images: {e}") + + def add_empty_frame(self): + """Add an empty frame with an optional background color. If there are no frames, prompt for the size of the new frame.""" + if not self.frames: + # Prompt the user for the width and height if no frames exist + width = simpledialog.askinteger("Frame Size", "Enter frame width:", minvalue=1) + height = simpledialog.askinteger("Frame Size", "Enter frame height:", minvalue=1) + if not width or not height: + messagebox.showerror("Invalid Input", "Width and height must be positive integers.") + return + frame_size = (width, height) + + # Prompt the user for the background color in hexadecimal format + color_code = simpledialog.askstring("Add Empty Frame", "Enter background color (hex code, e.g., #FFFFFF for white):") + else: + # Use the size of the existing frames + frame_size = self.frames[0].size + + # Prompt the user for the background color in hexadecimal format + color_code = simpledialog.askstring("Add Empty Frame", "Enter background color (hex code, e.g., #FFFFFF for white):") + + # Validate and set the color, default to transparent if invalid + if color_code and len(color_code) == 7 and color_code[0] == '#': + try: + # Test the color code by creating a single pixel image + Image.new("RGBA", (1, 1), color_code).verify() + except ValueError: + messagebox.showerror("Invalid Color", "The entered color code is invalid. Using transparent background instead.") + color_code = None + else: + color_code = None + + self.save_state() # Save the state before making changes + + # Create a new empty frame with the specified or default color + try: + new_frame = Image.new("RGBA", frame_size, color_code if color_code else (0, 0, 0, 0)) + except Exception as e: + messagebox.showerror("Error", f"Failed to create a new frame: {e}") + return + + # Add the new frame to the list of frames + self.frames.append(new_frame) + self.delays.append(100) # Default delay for new frame + var = IntVar() + var.trace_add('write', lambda *args, i=len(self.checkbox_vars): self.set_current_frame(i)) + self.checkbox_vars.append(var) + + self.update_frame_list() + self.show_frame() + + def delete_frames(self, event=None): + """Delete the selected frames.""" + if not self.frames: + messagebox.showerror("Error", "No frames to delete.") + return + + self.save_state() # Save the state before making changes + indices_to_delete = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] + + if not indices_to_delete: + messagebox.showinfo("Info", "No frames selected for deletion.") + return + + for index in reversed(indices_to_delete): + del self.frames[index] + del self.delays[index] + del self.checkbox_vars[index] + + # Update frame_index to ensure it is within the correct bounds + if self.frame_index >= len(self.frames): + self.frame_index = max(0, len(self.frames) - 1) + + self.update_frame_list() + self.show_frame() # Update the frame display + + def prompt_and_move_selected_frames(self): + """Prompt the user for the target position and move the selected frames.""" + if not self.frames: + messagebox.showerror("Error", "No frames available to move.") + return + + target_position = simpledialog.askinteger("Move Frames", "Enter the target position (0-based index):", + minvalue=0, maxvalue=len(self.frames) - 1) + if target_position is not None: + self.move_selected_frames(target_position) + + def move_selected_frames(self, target_position): + """ + Move selected frames to a specific target position. + + Parameters: + - target_position (int): The position where the selected frames should be moved. + + This function moves all the frames with checkboxes checked to the specified target position in a safe and consistent manner. + """ + if not self.frames: + messagebox.showerror("Error", "No frames available to move.") + return + + if target_position < 0 or target_position >= len(self.frames): + messagebox.showerror("Error", "Invalid target position.") + return + + selected_indices = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] + + if not selected_indices: + messagebox.showinfo("Info", "No frames selected to move.") + return + + self.save_state() # Save the state before making changes + + # Get the selected frames and their delays + selected_frames = [self.frames[i] for i in selected_indices] + selected_delays = [self.delays[i] for i in selected_indices] + + # Remove selected frames from their original positions + for index in reversed(selected_indices): + del self.frames[index] + del self.delays[index] + del self.checkbox_vars[index] + + # Insert selected frames at the target position + for i, (frame, delay) in enumerate(zip(selected_frames, selected_delays)): + insert_position = target_position + i + self.frames.insert(insert_position, frame) + self.delays.insert(insert_position, delay) + var = IntVar(value=1) # Check the checkbox for the moved frames + var.trace_add('write', lambda *args, i=insert_position: self.set_current_frame(i)) + self.checkbox_vars.insert(insert_position, var) + + self.update_frame_list() + self.show_frame() + + def move_frame_up(self, event=None): + """Move the selected frame up in the list.""" + selected_indices = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] + + if len(selected_indices) != 1: + messagebox.showwarning("Selection Error", "Please select exactly one frame to move.") + return + + selected_index = selected_indices[0] + + if selected_index == 0: + messagebox.showinfo("Move Up", "The selected frame is already at the top.") + return + + self.save_state() # Save the state before making changes + + # Swap the frames and delays + self.frames[selected_index], self.frames[selected_index - 1] = self.frames[selected_index - 1], self.frames[selected_index] + self.delays[selected_index], self.delays[selected_index - 1] = self.delays[selected_index - 1], self.delays[selected_index] + + # Move the check state + self.checkbox_vars[selected_index].set(0) + self.checkbox_vars[selected_index - 1].set(1) + + # Update the frame index to the new position of the moved frame + self.frame_index = selected_index - 1 + + self.update_frame_list() + self.show_frame() + + def move_frame_down(self, event=None): + """Move the selected frame down in the list.""" + selected_indices = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] + + if len(selected_indices) != 1: + messagebox.showwarning("Selection Error", "Please select exactly one frame to move.") + return + + selected_index = selected_indices[0] + + if selected_index == len(self.frames) - 1: + messagebox.showinfo("Move Down", "The selected frame is already at the bottom.") + return + + self.save_state() # Save the state before making changes + + # Swap the frames and delays + self.frames[selected_index], self.frames[selected_index + 1] = self.frames[selected_index + 1], self.frames[selected_index] + self.delays[selected_index], self.delays[selected_index + 1] = self.delays[selected_index + 1], self.delays[selected_index] + + # Move the check state + self.checkbox_vars[selected_index].set(0) + self.checkbox_vars[selected_index + 1].set(1) + + # Update the frame index to the new position of the moved frame + self.frame_index = selected_index + 1 + + self.update_frame_list() + self.show_frame() + + def rotate_selected_frames_180(self): + """Rotate the selected frames 180 degrees.""" + self.save_state() # Save the state before making changes + for i, frame in enumerate(self.frames): + if self.checkbox_vars[i].get() == 1: + self.frames[i] = frame.rotate(180) + self.update_frame_list() + self.show_frame() + + def rotate_selected_frames_90_cw(self): + """Rotate the selected frames 90 degrees clockwise.""" + self.save_state() # Save the state before making changes + for i, frame in enumerate(self.frames): + if self.checkbox_vars[i].get() == 1: + self.frames[i] = frame.rotate(-90, expand=True) + self.update_frame_list() + self.show_frame() + + def rotate_selected_frames_90_ccw(self): + """Rotate the selected frames 90 degrees counterclockwise.""" + self.save_state() # Save the state before making changes + for i, frame in enumerate(self.frames): + if self.checkbox_vars[i].get() == 1: + self.frames[i] = frame.rotate(90, expand=True) + self.update_frame_list() + self.show_frame() + + def rotate_selected_frames(self): + """Rotate the selected frames by a user-specified number of degrees.""" + try: + angle = simpledialog.askfloat("Rotate Frames", "Enter the rotation angle in degrees:", parent=self.master) + if angle is None: # User canceled the dialog + return + + self.save_state() # Save the state before making changes + + for i, frame in enumerate(self.frames): + if self.checkbox_vars[i].get() == 1: + self.frames[i] = frame.rotate(angle, expand=True) + + self.update_frame_list() + self.show_frame() + + except ValueError: + messagebox.showerror("Invalid Input", "Please enter a valid number for the rotation angle.") + + def flip_selected_frames_horizontal(self): + """Flip the selected frames horizontally.""" + self.save_state() # Save the state before making changes + for i, frame in enumerate(self.frames): + if self.checkbox_vars[i].get() == 1: + self.frames[i] = frame.transpose(Image.FLIP_LEFT_RIGHT) + self.update_frame_list() + self.show_frame() + + def flip_selected_frames_vertical(self): + """Flip the selected frames vertically.""" + self.save_state() # Save the state before making changes + for i, frame in enumerate(self.frames): + if self.checkbox_vars[i].get() == 1: + self.frames[i] = frame.transpose(Image.FLIP_TOP_BOTTOM) + self.update_frame_list() + self.show_frame() + + def crop_frames(self): + """Crop the selected frames based on user input values for each side.""" + selected_indices = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] + if not selected_indices: + messagebox.showinfo("Info", "No frames selected for cropping.") + return + + # Prompt user for crop values + try: + crop_left = int(simpledialog.askstring("Crop", "Enter pixels to crop from the left:", parent=self.master)) + crop_right = int(simpledialog.askstring("Crop", "Enter pixels to crop from the right:", parent=self.master)) + crop_top = int(simpledialog.askstring("Crop", "Enter pixels to crop from the top:", parent=self.master)) + crop_bottom = int(simpledialog.askstring("Crop", "Enter pixels to crop from the bottom:", parent=self.master)) + except (TypeError, ValueError): + messagebox.showerror("Invalid Input", "Please enter valid integers for cropping values.") + return + + # Validate crop values + if crop_left < 0 or crop_right < 0 or crop_top < 0 or crop_bottom < 0: + messagebox.showerror("Invalid Input", "Crop values must be non-negative integers.") + return + + self.save_state() # Save the state before making changes + + for index in selected_indices: + frame = self.frames[index] + width, height = frame.size + left = max(0, crop_left) + top = max(0, crop_top) + right = width - max(0, crop_right) + bottom = height - max(0, crop_bottom) + + if right <= left or bottom <= top: + messagebox.showerror("Invalid Crop Values", "Cropping values are too large.") + return + + cropped_frame = frame.crop((left, top, right, bottom)) + self.frames[index] = cropped_frame + + self.update_frame_list() + self.show_frame() + + def resize_frames_dialog(self): + """Open a simple dialog to get new size and resize all frames.""" + if not any(var.get() for var in self.checkbox_vars): + messagebox.showinfo("info", "No frames are selected for resizing.") + return + + width = simpledialog.askinteger("Input", "Enter new width:", parent=self.master, minvalue=1) + height = simpledialog.askinteger("Input", "Enter new height:", parent=self.master, minvalue=1) + + if width and height: + self.resize_frames(width, height) + + def resize_frames(self, new_width, new_height): + """Resize all checked frames to the specified width and height.""" + self.save_state() + for i, frame in enumerate(self.frames): + if self.checkbox_vars[i].get(): + self.frames[i] = frame.resize((new_width, new_height), Image.LANCZOS) + self.update_frame_list() + self.show_frame() + + def copy_frames(self, event=None): + """Copy the selected frames to the clipboard.""" + self.copied_frames = [(self.frames[i].copy(), self.delays[i]) for i in range(len(self.checkbox_vars)) if self.checkbox_vars[i].get() == 1] + if not self.copied_frames: + messagebox.showinfo("Info", "No frames selected to copy.") + else: + messagebox.showinfo("Info", f"Copied {len(self.copied_frames)} frame(s).") + + def paste_frames(self, event=None): + """Paste the copied frames below the selected frames with all checkboxes checked.""" + # Check if there are any frames copied + if not hasattr(self, 'copied_frames') or not self.copied_frames: + messagebox.showerror("Error", "No frames to paste. Please copy frames first.") + return + + # Get the selected indices + selected_indices = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] + if not selected_indices: + messagebox.showinfo("Info", "No frames selected to paste after. Pasting at the end.") + insert_index = len(self.frames) + else: + insert_index = max(selected_indices) + 1 + + # Save the current state for undo functionality + self.save_state() + + # Insert the copied frames and delays at the specified index + for frame, delay in self.copied_frames: + self.frames.insert(insert_index, frame) + self.delays.insert(insert_index, delay) + var = IntVar(value=1) # Set the checkbox to be checked by default + var.trace_add('write', lambda *args, i=insert_index: self.set_current_frame(i)) + self.checkbox_vars.insert(insert_index, var) + insert_index += 1 + + # Update the frame list and display the current frame + self.update_frame_list() + self.show_frame() + + def undo(self, event=None): + """Undo the last action.""" + if self.history: + self.redo_stack.append((self.frames.copy(), self.delays.copy(), [var.get() for var in self.checkbox_vars], self.frame_index, self.current_file)) + self.frames, self.delays, checkbox_states, self.frame_index, self.current_file = self.history.pop() + self.checkbox_vars = [IntVar(value=state) for state in checkbox_states] + for i, var in enumerate(self.checkbox_vars): + var.trace_add('write', lambda *args, i=i: self.set_current_frame(i)) + self.base_size = self.frames[0].size if self.frames else None # Reset base size based on the remaining frames + self.update_frame_list() + self.show_frame() + self.update_title() + self.check_all.set(False) # Reset the check_all variable to ensure consistency + + def redo(self, event=None): + """Redo the last undone action.""" + if self.redo_stack: + self.history.append((self.frames.copy(), self.delays.copy(), [var.get() for var in self.checkbox_vars], self.frame_index, self.current_file)) + self.frames, self.delays, checkbox_states, self.frame_index, self.current_file = self.redo_stack.pop() + self.checkbox_vars = [IntVar(value=state) for state in checkbox_states] + for i, var in enumerate(self.checkbox_vars): + var.trace_add('write', lambda *args, i=i: self.set_current_frame(i)) + self.update_frame_list() + self.show_frame() + self.update_title() + self.check_all.set(False) # Reset the check_all variable to ensure consistency + + def toggle_check_all(self, event=None): + """Toggle all checkboxes in the frame list without scrolling or changing the displayed frame.""" + self.save_state() # Save the state before making changes + new_state = not self.check_all.get() + self.check_all.set(new_state) + + # Temporarily remove traces + for var in self.checkbox_vars: + var.trace_remove('write', var.trace_info()[0][1]) + + for var in self.checkbox_vars: + var.set(1 if new_state else 0) + + # Re-add traces + for i, var in enumerate(self.checkbox_vars): + var.trace_add('write', lambda *args, i=i: self.set_current_frame(i)) + + self.update_frame_list() + + def toggle_play_pause(self, event=None): + """Toggle play/pause for the animation.""" + if self.is_playing: + self.stop_animation() + else: + self.play_animation() + + def play_animation(self): + """Play the GIF animation.""" + self.is_playing = True + self.play_button.config(text="Stop") + self.play_next_frame() + + def stop_animation(self): + """Stop the GIF animation.""" + self.is_playing = False + self.play_button.config(text="Play") + + def play_next_frame(self): + """Play the next frame in the animation.""" + if self.is_playing and self.frames: + self.show_frame() + delay = self.delays[self.frame_index] + self.frame_index = (self.frame_index + 1) % len(self.frames) + self.master.after(delay, self.play_next_frame) + + def set_delay(self, event=None): + """Set the delay for the selected frames.""" + try: + delay = int(self.delay_entry.get()) + self.save_state() # Save the state before making changes + for i, var in enumerate(self.checkbox_vars): + if var.get() == 1: + self.delays[i] = delay + self.update_frame_list() + except ValueError: + messagebox.showerror("Invalid Input", "Please enter a valid integer for delay.") + + def focus_delay_entry(self, event=None): + """Set focus to the delay entry field and scroll to the current frame.""" + self.delay_entry.focus_set() + self.focus_current_frame() + + def focus_current_frame(self): + """Ensure the current frame is visible in the frame list.""" + if self.frame_list.winfo_children(): + frame_widgets = self.frame_list.winfo_children() + current_frame_widget = frame_widgets[self.frame_index] + self.canvas.yview_moveto(current_frame_widget.winfo_y() / self.canvas.bbox("all")[3]) + + def show_about(self): + """Display the About dialog.""" + messagebox.showinfo("About GIFCraft", "GIFCraft - GIF Editor\nVersion 1.0\n© 2024 by Seehrum") + def setup_frame_list(self): """Set up the frame list with scrollbar.""" - self.frame_list_frame = tk.Frame(self.master) + self.frame_list_frame = Frame(self.master) self.frame_list_frame.pack(side=tk.LEFT, fill=tk.Y) self.scrollbar = Scrollbar(self.frame_list_frame) self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - self.canvas = tk.Canvas(self.frame_list_frame, yscrollcommand=self.scrollbar.set) + self.canvas = Canvas(self.frame_list_frame, yscrollcommand=self.scrollbar.set) self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) self.scrollbar.config(command=self.canvas.yview) - self.frame_list = tk.Frame(self.canvas) + self.frame_list = Frame(self.canvas) self.canvas.create_window((0, 0), window=self.frame_list, anchor='nw') def setup_control_frame(self): @@ -167,8 +775,6 @@ class GIFEditor: self.control_frame.update_idletasks() self.control_frame_canvas.config(scrollregion=self.control_frame.bbox("all")) - - def validate_delay(self, new_value): """Validate that the delay entry contains only digits.""" if new_value.isdigit() or new_value == "": @@ -198,73 +804,31 @@ class GIFEditor: self.master.bind("", self.redo) self.master.bind("", self.redo) self.master.bind("", self.save) - self.master.bind("", self.save) + self.master.bind("", self.save_as) + self.master.bind("x", self.toggle_checkbox) + self.master.bind("X", self.toggle_checkbox) self.master.bind("a", self.toggle_check_all) self.master.bind("A", self.toggle_check_all) self.master.bind("d", self.focus_delay_entry) self.master.bind("D", self.focus_delay_entry) - self.master.bind("x", self.toggle_checkbox) - self.master.bind("X", self.toggle_checkbox) - def toggle_play_pause(self, event=None): - """Toggle play/pause for the animation.""" - if self.is_playing: - self.stop_animation() - else: - self.play_animation() + def toggle_checkbox(self, event=None): + """Toggle the checkbox of the current frame.""" + if self.checkbox_vars: + current_var = self.checkbox_vars[self.frame_index] + current_var.set(0 if current_var.get() else 1) def previous_frame(self, event=None): - """Show the previous frame.""" + """Show the previous frame without altering the scrollbar position.""" if self.frame_index > 0: self.frame_index -= 1 - self.show_frame(scroll=False) + self.show_frame() def next_frame(self, event=None): - """Show the next frame.""" + """Show the next frame without altering the scrollbar position.""" if self.frame_index < len(self.frames) - 1: self.frame_index += 1 - self.show_frame(scroll=False) - - def new_file(self, event=None): - """Create a new file, prompting to save unsaved changes if any.""" - if self.frames: - response = messagebox.askyesnocancel("Unsaved Changes", "Do you want to save the current file before creating a new one?") - if response: # Yes - self.save() - if self.frames: # If saving was cancelled or failed, do not proceed - return - elif response is None: # Cancel - return - - self.reset_editor_state() - - def load_file(self, event=None): - """Load a GIF, PNG, or WebP file and extract its frames.""" - file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.gif *.png *.webp")]) - if not file_path: - return - - self.save_state() # Save the state before making changes - self.reset_frames_and_vars() - - try: - with Image.open(file_path) as img: - for i, frame in enumerate(ImageSequence.Iterator(img)): - if i == 0: - self.base_size = frame.size - self.frames.append(self.resize_to_base_size(frame.copy())) - delay = int(frame.info.get('duration', 100)) - self.delays.append(delay) - var = IntVar() - var.trace_add('write', lambda *args, i=len(self.checkbox_vars): self.set_current_frame(i)) - self.checkbox_vars.append(var) - self.frame_index = 0 - self.update_frame_list() self.show_frame() - self.current_file = file_path - self.update_title() - except Exception as e: - messagebox.showerror("Error", f"Failed to load file: {e}") def resize_to_base_size(self, image): """Resize the image to the base size of the first frame and center it.""" @@ -276,139 +840,42 @@ class GIFEditor: return new_image return image - def resize_frames_dialog(self): - """Open a simple dialog to get new size and resize all frames.""" - if not any(var.get() for var in self.checkbox_vars): - messagebox.showinfo("info", "No frames are selected for resizing.") - return - - width = simpledialog.askinteger("Input", "Enter new width:", parent=self.master, minvalue=1) - height = simpledialog.askinteger("Input", "Enter new height:", parent=self.master, minvalue=1) - - if width and height: - self.resize_frames(width, height) - - def resize_frames(self, new_width, new_height): - """Resize all checked frames to the specified width and height.""" - self.save_state() - for i, frame in enumerate(self.frames): - if self.checkbox_vars[i].get(): - self.frames[i] = frame.resize((new_width, new_height), Image.LANCZOS) - self.update_frame_list() - self.show_frame() - - def resize_image(self, image, max_width=800, max_height=600): - """Resize the image to fit within the specified max width and height.""" - width, height = image.size - if width > max_width or height > max_height: - scaling_factor = min(max_width / width, max_height / height) - new_size = (int(width * scaling_factor), int(height * scaling_factor)) - image = image.resize(new_size, Image.Resampling.LANCZOS) - return image - - def add_image(self): - """Add images to the frames.""" - file_paths = filedialog.askopenfilenames(filetypes=[("Image files", "*.jpg *.jpeg *.png *.webp *.gif *.bmp")]) - if not file_paths: - return - - self.save_state() # Save the state before making changes - try: - for file_path in file_paths: - with Image.open(file_path) as image: - if not self.frames: - self.base_size = image.size - image = self.resize_to_base_size(image.copy()) - self.frames.append(image) - self.delays.append(100) - var = IntVar() - var.trace_add('write', lambda *args, i=len(self.checkbox_vars): self.set_current_frame(i)) - self.checkbox_vars.append(var) - - self.update_frame_list() - self.show_frame() - except Exception as e: - messagebox.showerror("Error", f"Failed to add images: {e}") - - def add_empty_frame(self): - """Add an empty frame with optional background color.""" - if not self.frames: - messagebox.showerror("Error", "No frames available to determine the size for the new frame.") - return - - # Prompt the user for the background color in hexadecimal format - color_code = simpledialog.askstring("Add Empty Frame", "Enter background color (hex code, e.g., #FFFFFF for white):") - - # Validate and set the color, default to transparent if invalid - if color_code and len(color_code) == 7 and color_code[0] == '#': - try: - # Test the color code by creating a single pixel image - Image.new("RGBA", (1, 1), color_code).verify() - except ValueError: - messagebox.showerror("Invalid Color", "The entered color code is invalid. Using transparent background instead.") - color_code = None - else: - color_code = None - - self.save_state() # Save the state before making changes - - # Create a new empty frame with the specified or default color - try: - new_frame = Image.new("RGBA", self.frames[0].size, color_code if color_code else (0, 0, 0, 0)) - except IndexError as e: - messagebox.showerror("Error", f"Failed to create a new frame: {e}") - return - - self.frames.append(new_frame) - self.delays.append(100) # Default delay for new frame - var = IntVar() - var.trace_add('write', lambda *args, i=len(self.checkbox_vars): self.set_current_frame(i)) - self.checkbox_vars.append(var) - - self.update_frame_list() - self.show_frame() - - - def resize_to_max_dimensions(self, image): - """Resize the image to the maximum dimensions of the current frames.""" - if not self.frames: - return image - - max_width = max(frame.width for frame in self.frames) - max_height = max(frame.height for frame in self.frames) - return image.resize((max_width, max_height), Image.Resampling.LANCZOS) - def update_frame_list(self): - """Update the listbox with the current frames and their delays.""" + """Update the frame list with the current frames and their delays.""" + # Clear existing widgets for widget in self.frame_list.winfo_children(): widget.destroy() + # Display message if no frames are available if not self.frames: - label = tk.Label(self.frame_list, text="No frames available") - label.pack() + tk.Label(self.frame_list, text="No frames available").pack() self.canvas.config(scrollregion=self.canvas.bbox("all")) return - for i, (frame, delay) in enumerate(zip(self.frames, self.delays)): - var = self.checkbox_vars[i] - bg_color = 'gray' if i == self.frame_index else self.master.cget('bg') - frame_widget = tk.Frame(self.frame_list, bg=bg_color) - frame_widget.pack(fill=tk.X) - checkbox = Checkbutton(frame_widget, variable=var, bg=bg_color, command=lambda i=i: self.toggle_frame(i)) + # Create and populate frame widgets + for i, (frame, delay, var) in enumerate(zip(self.frames, self.delays, self.checkbox_vars)): + frame_container = Frame(self.frame_list) + frame_container.pack(fill=tk.X) + + checkbox = Checkbutton(frame_container, variable=var) checkbox.pack(side=tk.LEFT) - label_text = f"→ Frame {i + 1}: {delay} ms" if i == self.frame_index else f"Frame {i + 1}: {delay} ms" - label = tk.Label(frame_widget, text=label_text, bg=bg_color) + + frame_label_text = f"Frame {i + 1}: {delay} ms" + if i == self.frame_index: + frame_label_text = f"→ {frame_label_text}" + + label = tk.Label(frame_container, text=frame_label_text) label.pack(side=tk.LEFT, fill=tk.X) - self.canvas.update_idletasks() + # Update the scroll region self.canvas.config(scrollregion=self.canvas.bbox("all")) def set_current_frame(self, index): """Set the current frame to the one corresponding to the clicked checkbox.""" self.frame_index = index - self.show_frame(scroll=False) + self.show_frame() - def show_frame(self, scroll=True): + def show_frame(self): """Display the current frame.""" if self.frames: frame = self.frames[self.frame_index] @@ -416,487 +883,32 @@ class GIFEditor: photo = ImageTk.PhotoImage(preview) self.image_label.config(image=photo) self.image_label.image = photo - self.image_label.config(text='') + self.image_label.config(text='') # Remove text when showing image self.delay_entry.delete(0, tk.END) self.delay_entry.insert(0, str(self.delays[self.frame_index])) - self.dimension_label.config(text=f"Size: {frame.width}x{frame.height}") + self.dimension_label.config(text=f"Size: {frame.width}x{frame.height}") # Show frame dimensions total_duration = sum(self.delays) - self.total_duration_label.config(text=f"Total Duration: {total_duration} ms") + self.total_duration_label.config(text=f"Total Duration: {total_duration} ms") # Show total duration else: self.image_label.config(image='', text="No frames to display") self.image_label.image = None self.delay_entry.delete(0, tk.END) - self.dimension_label.config(text="") - self.total_duration_label.config(text="") - self.update_frame_list() - if scroll: - self.focus_current_frame() - - def delete_frames(self, event=None): - """Delete the selected frames.""" - if not self.frames: - messagebox.showerror("Error", "No frames to delete.") - return - - self.save_state() # Save the state before making changes - indices_to_delete = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] - - if not indices_to_delete: - messagebox.showinfo("Info", "No frames selected for deletion.") - return - - for index in reversed(indices_to_delete): - del self.frames[index] - del self.delays[index] - del self.checkbox_vars[index] - - if self.frame_index >= len(self.frames): - self.frame_index = max(0, len(self.frames) - 1) - - self.update_frame_list() - self.show_frame() - - def move_frames_to_position(self): - """Move selected frames below the frame with the specified name.""" - # Check if any frames are selected to move - if not any(var.get() for var in self.checkbox_vars): - messagebox.showinfo("info", "No frames selected to move.") - return - - frame_name = tk.simpledialog.askstring("Move Frames", "Enter the name of the frame to move below (e.g., Frame 1):") - if not frame_name: - return - - try: - frame_number = int(frame_name.split()[1]) - except (IndexError, ValueError): - messagebox.showerror("Invalid Input", "Please enter a valid frame name (e.g., Frame 1).") - return - - if frame_number < 1 or frame_number > len(self.frames): - messagebox.showerror("Invalid Input", "Frame number out of range.") - return - - target_index = frame_number - 1 - selected_indices = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] - - if not selected_indices: - messagebox.showinfo("Info", "No frames selected to move.") - return - - self.save_state() # Save the state before making changes - - # Sort indices to preserve the order when re-inserting - selected_indices.sort() - - # Collect the frames, delays, and checkboxes to be moved - frames_to_move = [self.frames[i] for i in selected_indices] - delays_to_move = [self.delays[i] for i in selected_indices] - checkboxes_to_move = [self.checkbox_vars[i] for i in selected_indices] - - # Remove selected frames from the original positions - for i in reversed(selected_indices): - del self.frames[i] - del self.delays[i] - del self.checkbox_vars[i] - - # Insert the frames, delays, and checkboxes at the target position - for i, (frame, delay, checkbox) in enumerate(zip(frames_to_move, delays_to_move, checkboxes_to_move)): - insertion_index = target_index + 1 + i - self.frames.insert(insertion_index, frame) - self.delays.insert(insertion_index, delay) - self.checkbox_vars.insert(insertion_index, checkbox) - - self.update_frame_list() - self.show_frame() - - def move_frame_up(self, event=None): - """Move the selected frames up in the list.""" - self.save_state() # Save the state before making changes - indices_to_move = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] - for i in indices_to_move: - if i > 0 and i - 1 not in indices_to_move: - self.swap_frames(i, i - 1) - if i == self.frame_index: - self.frame_index = i - 1 - elif i - 1 == self.frame_index: - self.frame_index = i - - self.show_frame() - - def move_frame_down(self, event=None): - """Move the selected frames down in the list.""" - self.save_state() # Save the state before making changes - indices_to_move = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] - for i in reversed(indices_to_move): - if i < len(self.frames) - 1 and i + 1 not in indices_to_move: - self.swap_frames(i, i + 1) - if i == self.frame_index: - self.frame_index = i + 1 - elif i + 1 == self.frame_index: - self.frame_index = i - - self.show_frame() - - def swap_frames(self, i, j): - """Swap frames and update indexes.""" - self.frames[i], self.frames[j] = self.frames[j], self.frames[i] - self.delays[i], self.delays[j] = self.delays[j], self.delays[i] - self.checkbox_vars[i].set(0) - self.checkbox_vars[j].set(1) - if i == self.frame_index: - self.frame_index = j - elif j == self.frame_index: - self.frame_index = i - - def play_animation(self): - """Play the GIF animation.""" - self.is_playing = True - self.play_button.config(text="Stop") - self.play_next_frame() - - def stop_animation(self): - """Stop the GIF animation.""" - self.is_playing = False - self.play_button.config(text="Play") - - def play_next_frame(self): - """Play the next frame in the animation.""" - if self.is_playing and self.frames: - self.show_frame(scroll=False) - delay = self.delays[self.frame_index] - self.frame_index = (self.frame_index + 1) % len(self.frames) - self.master.after(delay, self.play_next_frame) - - def set_delay(self, event=None): - """Set the delay for the selected frames.""" - try: - delay = int(self.delay_entry.get()) - self.save_state() # Save the state before making changes - for i, var in enumerate(self.checkbox_vars): - if var.get() == 1: - self.delays[i] = delay - self.update_frame_list() - except ValueError: - messagebox.showerror("Invalid Input", "Please enter a valid integer for delay.") - - def focus_delay_entry(self, event=None): - """Set focus to the delay entry field and scroll to the current frame.""" - self.delay_entry.focus_set() - self.focus_current_frame() - - def focus_current_frame(self): - """Ensure the current frame is visible in the frame list.""" - if self.frame_list.winfo_children(): - frame_widgets = self.frame_list.winfo_children() - current_frame_widget = frame_widgets[self.frame_index] - self.canvas.yview_moveto(current_frame_widget.winfo_y() / self.canvas.bbox("all")[3]) - - def save(self, event=None): - """Save the current frames and delays to a GIF file.""" - if self.current_file: - self.save_to_file(self.current_file) - else: - self.save_as() - - def save_as_high_quality_gif(self): - """Save the current frames and delays to a high-quality GIF file using dithering.""" - file_path = filedialog.asksaveasfilename(defaultextension=".gif", filetypes=[("GIF files", "*.gif")]) - if file_path: - try: - images = [frame.convert("RGB").quantize(method=0) for frame in self.frames] - images[0].save(file_path, save_all=True, append_images=images[1:], duration=self.delays, loop=0, dither=Image.NONE) - self.current_file = file_path - self.update_title() - messagebox.showinfo("Success", "High-quality GIF saved successfully!") - except Exception as e: - messagebox.showerror("Error", f"Failed to save high-quality GIF: {e}") - - def save_as(self, event=None): - """Save the current frames and delays to a file with the selected format.""" - file_path = filedialog.asksaveasfilename(defaultextension=".gif", filetypes=[("GIF files", "*.gif"), ("PNG files", "*.png"), ("WebP files", "*.webp")]) - if file_path: - self.save_to_file(file_path) - self.current_file = file_path - self.update_title() - - def save_to_file(self, file_path): - """Save the frames and delays to the specified file in the given format.""" - if self.frames: - try: - _, ext = os.path.splitext(file_path) - ext = ext[1:].lower() - if ext == 'gif': - self.frames[0].save(file_path, save_all=True, append_images=self.frames[1:], duration=self.delays, loop=0) - elif ext == 'png': - self.frames[0].save(file_path, save_all=True, append_images=self.frames[1:], duration=self.delays, loop=0, format='PNG') - elif ext == 'webp': - self.frames[0].save(file_path, save_all=True, append_images=self.frames[1:], duration=self.delays, loop=0, format='WEBP') - else: - messagebox.showerror("Error", f"Unsupported file format: {ext.upper()}") - return - self.current_file = file_path - self.update_title() - messagebox.showinfo("Success", f"{ext.upper()} saved successfully!") - except Exception as e: - messagebox.showerror("Error", f"Failed to save {ext.upper()}: {e}") - - def extract_frames(self): - """Extract the frames and save them as individual images.""" - if not self.frames: - messagebox.showerror("Error", "No frames to extract.") - return - - folder_path = filedialog.askdirectory() - if not folder_path: - return - - try: - for i, frame in enumerate(self.frames): - frame_path = os.path.join(folder_path, f"frame_{i + 1}.png") - frame.save(frame_path) - messagebox.showinfo("Success", "Frames extracted successfully!") - except Exception as e: - messagebox.showerror("Error", f"Failed to extract frames: {e}") + self.dimension_label.config(text="") # Clear frame dimensions + self.total_duration_label.config(text="") # Clear total duration + self.update_frame_list() # Refresh the frame list to show the current frame indicator def save_state(self): """Save the current state for undo functionality.""" self.history.append((self.frames.copy(), self.delays.copy(), [var.get() for var in self.checkbox_vars], self.frame_index, self.current_file)) - self.redo_stack.clear() + self.redo_stack.clear() # Clear the redo stack on new action - def copy_frames(self, event=None): - """Copy the selected frames to the clipboard.""" - self.copied_frames = [(self.frames[i].copy(), self.delays[i]) for i in range(len(self.checkbox_vars)) if self.checkbox_vars[i].get() == 1] - if not self.copied_frames: - messagebox.showinfo("Info", "No frames selected to copy.") - else: - messagebox.showinfo("Info", f"Copied {len(self.copied_frames)} frame(s).") + def resize_image(self, image, max_width, max_height): + """Resize image while maintaining aspect ratio.""" + ratio = min(max_width / image.width, max_height / image.height) + new_width = int(image.width * ratio) + new_height = int(image.height * ratio) + return image.resize((new_width, new_height), Image.Resampling.LANCZOS) - def paste_frames(self, event=None): - """Paste the copied frames below the selected frames.""" - if not hasattr(self, 'copied_frames') or not self.copied_frames: - messagebox.showerror("Error", "No frames to paste. Please copy frames first.") - return - - selected_indices = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] - if not selected_indices: - messagebox.showinfo("Info", "No frames selected to paste after. Pasting at the end.") - insert_index = len(self.frames) - else: - insert_index = max(selected_indices) + 1 - - self.save_state() # Save the state before making changes - - for frame, delay in self.copied_frames: - self.frames.insert(insert_index, frame) - self.delays.insert(insert_index, delay) - var = IntVar() - var.trace_add('write', lambda *args, i=insert_index: self.set_current_frame(i)) - self.checkbox_vars.insert(insert_index, var) - insert_index += 1 - - self.update_frame_list() - self.show_frame() - - def undo(self, event=None): - """Undo the last action.""" - if self.history: - self.redo_stack.append((self.frames.copy(), self.delays.copy(), [var.get() for var in self.checkbox_vars], self.frame_index, self.current_file)) - self.restore_state(self.history.pop()) - self.check_all.set(False) - - def redo(self, event=None): - """Redo the last undone action.""" - if self.redo_stack: - self.history.append((self.frames.copy(), self.delays.copy(), [var.get() for var in self.checkbox_vars], self.frame_index, self.current_file)) - self.restore_state(self.redo_stack.pop()) - self.check_all.set(False) - - def apply_crossfade_effect(self): - """Apply crossfade effect between checked frames.""" - checked_indices = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] - - if len(checked_indices) < 2: - messagebox.showinfo("Info", "Need at least two checked frames to apply crossfade effect.") - return - - self.save_state() # Save the state before making changes - - crossfade_frames = [] - crossfade_delays = [] - - def blend_frames(frame1, frame2, alpha): - """Blend two frames with given alpha.""" - return Image.blend(frame1, frame2, alpha) - - for idx in range(len(checked_indices) - 1): - i = checked_indices[idx] - j = checked_indices[idx + 1] - - frame1 = self.frames[i] - frame2 = self.frames[j] - crossfade_frames.append(frame1) - crossfade_delays.append(self.delays[i]) - - # Generate crossfade frames - steps = 10 # Number of steps for the crossfade - for step in range(1, steps): - alpha = step / float(steps) - blended_frame = blend_frames(frame1, frame2, alpha) - crossfade_frames.append(blended_frame) - crossfade_delays.append(self.delays[i] // steps) - - # Insert crossfade frames and delays at the correct positions - for idx in range(len(checked_indices) - 1, -1, -1): - i = checked_indices[idx] - self.frames.pop(i) - self.delays.pop(i) - self.checkbox_vars.pop(i) - - insert_index = checked_indices[0] - for frame, delay in zip(crossfade_frames, crossfade_delays): - self.frames.insert(insert_index, frame) - self.delays.insert(insert_index, delay) - var = IntVar(value=1) - var.trace_add('write', lambda *args, i=insert_index: self.set_current_frame(i)) - self.checkbox_vars.insert(insert_index, var) - insert_index += 1 - - self.update_frame_list() - self.show_frame() - - def reverse_frames(self): - """Apply reverse effect to the selected frames.""" - self.save_state() # Save the state before making changes - indices_to_reverse = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] - - if not indices_to_reverse: - messagebox.showinfo("Info", "No frames selected for reversing.") - return - - # Extract the selected frames and their delays - frames_to_reverse = [self.frames[i] for i in indices_to_reverse] - delays_to_reverse = [self.delays[i] for i in indices_to_reverse] - - # Reverse the selected frames and their delays - frames_to_reverse.reverse() - delays_to_reverse.reverse() - - # Replace the selected frames and their delays with the reversed versions - for idx, i in enumerate(indices_to_reverse): - self.frames[i] = frames_to_reverse[idx] - self.delays[i] = delays_to_reverse[idx] - - self.show_frame() - self.update_frame_list() - - def crop_frames(self): - """Crop the selected frames based on user input values for each side.""" - selected_indices = [i for i, var in enumerate(self.checkbox_vars) if var.get() == 1] - if not selected_indices: - messagebox.showinfo("Info", "No frames selected for cropping.") - return - - # Prompt user for crop values - try: - crop_left = int(simpledialog.askstring("Crop", "Enter pixels to crop from the left:", parent=self.master)) - crop_right = int(simpledialog.askstring("Crop", "Enter pixels to crop from the right:", parent=self.master)) - crop_top = int(simpledialog.askstring("Crop", "Enter pixels to crop from the top:", parent=self.master)) - crop_bottom = int(simpledialog.askstring("Crop", "Enter pixels to crop from the bottom:", parent=self.master)) - except (TypeError, ValueError): - messagebox.showerror("Invalid Input", "Please enter valid integers for cropping values.") - return - - # Validate crop values - if crop_left < 0 or crop_right < 0 or crop_top < 0 or crop_bottom < 0: - messagebox.showerror("Invalid Input", "Crop values must be non-negative integers.") - return - - self.save_state() # Save the state before making changes - - for index in selected_indices: - frame = self.frames[index] - width, height = frame.size - left = max(0, crop_left) - top = max(0, crop_top) - right = width - max(0, crop_right) - bottom = height - max(0, crop_bottom) - - if right <= left or bottom <= top: - messagebox.showerror("Invalid Crop Values", "Cropping values are too large.") - return - - cropped_frame = frame.crop((left, top, right, bottom)) - self.frames[index] = cropped_frame - - self.update_frame_list() - self.show_frame() - - def restore_state(self, state): - """Restore the state of the editor.""" - self.frames, self.delays, checkbox_states, self.frame_index, self.current_file = state - self.checkbox_vars = [IntVar(value=state) for state in checkbox_states] - for i, var in enumerate(self.checkbox_vars): - var.trace_add('write', lambda *args, i=i: self.set_current_frame(i)) - self.base_size = self.frames[0].size if self.frames else None - self.update_frame_list() - self.show_frame() - self.update_title() - - def toggle_check_all(self, event=None): - """Toggle all checkboxes in the frame list without scrolling or changing the displayed frame.""" - self.save_state() # Save the state before making changes - new_state = not self.check_all.get() - self.check_all.set(new_state) - - # Temporarily remove traces - for var in self.checkbox_vars: - var.trace_remove('write', var.trace_info()[0][1]) - - for var in self.checkbox_vars: - var.set(1 if new_state else 0) - - # Re-add traces - for i, var in enumerate(self.checkbox_vars): - var.trace_add('write', lambda *args, i=i: self.set_current_frame(i)) - - self.update_frame_list() - - def toggle_frame(self, index): - """Toggle the checkbox of the frame without scrolling.""" - self.checkbox_vars[index].set(0 if self.checkbox_vars[index].get() else 1) - self.update_frame_list() - - def toggle_checkbox(self, event=None): - """Toggle the checkbox of the current frame without scrolling.""" - if self.checkbox_vars: - current_var = self.checkbox_vars[self.frame_index] - current_var.set(0 if current_var.get() else 1) - self.update_frame_list() - - def show_about(self): - """Display the About dialog.""" - messagebox.showinfo("About GIFCraft", "GIFCraft - GIF Editor\nVersion 1.0\n© 2024 by Seehrum") - - def reset_editor_state(self): - """Reset the editor to its initial state.""" - self.frames = [] - self.delays = [] - self.checkbox_vars = [] - self.current_file = None - self.frame_index = 0 - self.base_size = None - self.update_frame_list() - self.show_frame() - self.update_title() - - def reset_frames_and_vars(self): - """Reset frames and associated variables.""" - self.frames = [] - self.delays = [] - self.checkbox_vars = [] - for widget in self.frame_list.winfo_children(): - widget.destroy() def main(): """Main function to initialize the GIF editor.""" @@ -908,5 +920,6 @@ def main(): print("Program interrupted with Ctrl+C") root.destroy() + if __name__ == "__main__": main()