diff --git a/ProjectFiles/V2-DisplayKeys-IS_new.py b/ProjectFiles/V2-DisplayKeys-IS_new.py new file mode 100644 index 0000000..72e6d73 --- /dev/null +++ b/ProjectFiles/V2-DisplayKeys-IS_new.py @@ -0,0 +1,452 @@ +from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget +from PyQt6.QtCore import Qt, QSize +from PyQt6.QtGui import QStandardItemModel, QPainter, QPixmap, QPen, QColor +from PyQt6.uic import loadUi +import sys, os +import tkinter as tk +from tkinter import filedialog, messagebox +from PIL import Image, ImageTk, ImageSequence + + +# Preview section constructor +class ImageSplitterPreview(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.image_path = None + self.rows = 0 + self.columns = 0 + self.gap = 0 + + def paintEvent(self, event): + if self.image_path is None: + return + + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + # Load the image + pixmap = QPixmap(self.image_path) + + # Rescale image to fit within preview boundaries + if pixmap.height() == 0: + return + else: + aspect_ratio = pixmap.width() / pixmap.height() + preview_width = self.width() + preview_height = self.height() + if preview_width / preview_height >= aspect_ratio: + # Constrained by height + new_height = preview_height + new_width = int(preview_height * aspect_ratio) + else: + # Constrained by width + new_width = preview_width + new_height = int(preview_width / aspect_ratio) + scaled_pixmap = pixmap.scaled( + new_width, new_height, Qt.AspectRatioMode.KeepAspectRatio + ) + + # Calculate offsets to center the image + x_offset = (preview_width - new_width) // 2 + y_offset = (preview_height - new_height) // 2 + + # Draw the scaled image + painter.drawPixmap(x_offset, y_offset, scaled_pixmap) + + # Draw red lines to indicate splitting + pen = QPen(QColor("red")) + pen.setWidth(self.gap) + painter.setPen(pen) + + if self.columns == 0 or self.rows == 0: + cell_width = 0 + cell_height = 0 + else: + cell_width = new_width // self.columns + cell_height = new_height // self.rows + + # Vertical lines + for i in range(1, self.columns): + x = x_offset + i * cell_width + painter.drawLine(x, y_offset, x, y_offset + new_height) + + # Horizontal lines + for i in range(1, self.rows): + y = y_offset + i * cell_height + painter.drawLine(x_offset, y, x_offset + new_width, y) + + def updatePreview(self, image_path, rows, columns, gap): + self.image_path = image_path + self.rows = rows + self.columns = columns + self.gap = gap + self.update() + + +class UI_MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + # Load the UI file + loadUi("gui_source.ui", self) + + # Initializing the custom preview widget + self.preview_frame = ImageSplitterPreview(self.preview_wrapper) + self.preview_frame.setMaximumSize(QSize(16777215, 16777215)) + self.preview_frame.setObjectName("preview_frame") + self.verticalLayout_6.addWidget(self.preview_frame) + + # Different widgets signal handling + self.comboBox.currentIndexChanged.connect(self.settings_visibilty) + self.browse_button.clicked.connect(self.browse_image) + self.browse_button_2.clicked.connect(self.browse_directory) + self.comboBox.currentIndexChanged.connect(self.settings_visibilty) + self.rows_box.valueChanged.connect(self.process_preview) + self.columns_box.valueChanged.connect(self.process_preview) + self.gap_box.valueChanged.connect(self.process_preview) + self.split_image_button.clicked.connect(self.process_image) + + self.settings_wrapper.hide() + + def button_clicked(self): + print("Button clicked!") + + def get_supported_types(self): + # The supported file formats: + sup_image_formats = [".png", ".jpg", ".jpeg", ".bmp"] + sup_animated_formats = [".gif"] + return sup_image_formats, sup_animated_formats + + # Grabs all required parameters and passes it to the DisplayKeys_Previewer widget. + # Currently, defaults are provided for any of the required inputs that are missing. + def process_preview(self): + print("---Processing Preview---") + + # Get Image Properties Type + get_params_type = self.comboBox.currentText() + + # Get Text boxes to process image + image_path = self.input_image_path.text() + output_dir = self.input_image_path_2.text() + rows = int(self.rows_box.value()) + columns = int(self.columns_box.value()) + gap = int(self.gap_box.value()) + + # Determine if the default or user-defined values should be used + if get_params_type == "Defaults": + image_path = image_path + if not image_path: + image_path = "assets/Preview.png" + rows = 2 + columns = 6 + gap = 40 + + elif get_params_type == "User Defined": + if not image_path: + image_path = "assets/Preview.png" + else: + return + + # Update the preview + self.preview_frame.updatePreview(image_path, rows, columns, gap) + + # Splits the provided GIF into GIF-Cell's based on provided parameters. + # This function crops the GIF's to make them square. + # This function preserves or adds Frame Timings in case Frame's are missing this information. + # Discards 0ms Frame Times. Default Frame Timing is 100ms, if only some Frame's have timing, average will be used. + def split_gif(gif_path, output_dir, rows, cols, gap): + # Create the output directory if it doesn't exist + os.makedirs(output_dir, exist_ok=True) + + # Open the image using PIL + gif = Image.open(gif_path) + print("GIF Frame Count: " + str(gif.n_frames)) + + # Extract frames from .gif file + frames = [] + for frame in ImageSequence.Iterator(gif): + frames.append(frame.copy()) + + # Get duration of each frame + frame_durations = [] + for frame in range(0, gif.n_frames): + gif.seek(frame) + try: + duration = int(gif.info["duration"]) + if duration > 0: + frame_durations.append(duration) + else: + frame_durations.append(0) + except (KeyError, TypeError): + print("No frame durations present") + + # Add default time in case no frame duration is provided by .gif + frame_durations.append(0) + + # Calculate average duration + non_zero_durations = [d for d in frame_durations if d > 0] + if len(non_zero_durations) > 0: + default_duration = sum(non_zero_durations) // len(non_zero_durations) + else: + default_duration = 100 + + # Replace missing values with average duration + for i in range(len(frame_durations)): + if frame_durations[i] == 0: + frame_durations[i] = default_duration + print("Frame Durations: \n" + frame_durations.__str__()) + + # Calculate the width and height of each image-cell + width, height = frames[0].size + cell_width = (width - (cols - 1) * gap) // cols + cell_height = (height - (rows - 1) * gap) // rows + + # Determine the maximum cell size (to maintain square format) + max_cell_size = min(cell_width, cell_height) + + # Calculate the horizontal and vertical offsets for cropping + horizontal_offset = (cell_width - max_cell_size) // 2 + vertical_offset = (cell_height - max_cell_size) // 2 + + # Determine the longest dimension (width or height) + longest_dimension = "width" if cell_width > cell_height else "height" + + # Split Frames + modified_frames = [] + for row in range(rows): + for col in range(cols): + # Calculate the coordinates for cropping + left = col * (cell_width + gap) + horizontal_offset + upper = row * (cell_height + gap) + vertical_offset + + # Remove rows/columns only if they are part of the Outlier image-cells + if row == 0: + upper += vertical_offset + elif row == rows - 1: + upper -= vertical_offset + if col == 0: + left += horizontal_offset + elif col == cols - 1: + left -= horizontal_offset + if longest_dimension == "width": + right = left + max_cell_size + lower = upper + cell_height + else: + right = left + cell_width + lower = upper + max_cell_size + + # Perform operation on each frame, for each row/column split image-cell + for frame in frames: + # Crop the current frame + image_cell = frame.crop((left, upper, right, lower)) + # Hold onto cropped image-cell + modified_frames.append(image_cell) + + # Generate the output file path + filename_without_extension = os.path.splitext( + os.path.basename(gif.filename) + )[0] + output_path = os.path.join( + output_dir, f"{filename_without_extension}_{row}_{col}.gif" + ) + print("Num of Modified Frames: " + str(modified_frames.__sizeof__())) + + # Save all frames of the image-cell into a single .gif file + modified_frames[0].save( + output_path, + save_all=True, + append_images=modified_frames[1:], + duration=frame_durations, # Might make this a user definable variable in the future + loop=0, + ) + modified_frames = [] + print( + f"gif_cell_{row}_{col} has this many frames: " + + str(Image.open(output_path).n_frames) + ) + print(f"Saved {output_path}") + + # Split the image + def split_image(self, image_path, output_dir, rows, cols, gap): + # Open the image using PIL + image = Image.open(image_path) + + # Calculate the width and height of each image-cell + width, height = image.size + print("Width:", width) + print("Height:", height) + cell_width = (width - (cols - 1) * gap) // cols + cell_height = (height - (rows - 1) * gap) // rows + + print("Cell Width:", cell_width) + print("Cell Height:", cell_height) + + # Create the output directory if it doesn't exist + os.makedirs(output_dir, exist_ok=True) + + # maximum cell size (to maintain square shape) + max_cell_size = min(cell_width, cell_height) + print("Max Cell Size:", max_cell_size) + + # Calculate the horizontal and vertical offsets for cropping + horizontal_offset = (cell_width - max_cell_size) // 2 + vertical_offset = (cell_height - max_cell_size) // 2 + + print("Horizontal Offset:", horizontal_offset) + print("Vertical Offset:", vertical_offset) + + # Determine the longest dimension (width or height) + longest_dimension = "width" if cell_width > cell_height else "height" + + # Split the image and save each image-cell + for row in range(rows): + for col in range(cols): + # Calculate the coordinates for cropping + left = col * (cell_width + gap) + horizontal_offset + upper = row * (cell_height + gap) + vertical_offset + + # Remove rows/columns only if they are part of the Outlier image-cells + if row == 0: + upper += vertical_offset + elif row == rows - 1: + upper -= vertical_offset + if col == 0: + left += horizontal_offset + elif col == cols - 1: + left -= horizontal_offset + if longest_dimension == "width": + right = left + max_cell_size + lower = upper + cell_height + else: + right = left + cell_width + lower = upper + max_cell_size + + # Crop all image-cells + image_cell = image.crop((left, upper, right, lower)) + + # Generate the output file path + filename_without_extension = os.path.splitext( + os.path.basename(image.filename) + )[0] + output_path = os.path.join( + output_dir, f"{filename_without_extension}_{row}_{col}.png" + ) + + # Save the image-cells + image_cell.save(output_path) + + print(f"Saved {output_path}") + + # Checks the provided image and determines whether it's a static or dynamic image format. + # Also passes along rest of variables provided by ButtonFunctions.ProcessImage + def determine_split_type(self, file_path, output_dir, rows, cols, gap): + print("---Determening File Type---") + + # The supported file formats: + image_formats = self.get_supported_types()[0] + animated_formats = self.get_supported_types()[1] + + print("File Types are: ") + print(self.get_supported_types()[0]) + print(self.get_supported_types()[1]) + + try: + # Check if image format is supported + with Image.open(file_path) as image: + print( + "Image can be opened: " + + ("True" if image else "False") + + "\n Image format is: " + + "." + + image.format.lower() + ) + # Is Image + if "." + image.format.lower() in image_formats: + self.split_image(file_path, output_dir, rows, cols, gap) + return True + # Is Animated + elif "." + image.format.lower() in animated_formats: + self.split_gif(file_path, output_dir, rows, cols, gap) + return True + else: + print("No formats matched") + + # Is not of a supported image format + except TypeError as error_message: + messagebox.showerror( + "Error" + f"File Format not supported\nCurrently supported formats are: \n- Image | {self.get_supported_types()[0]} \n- Animated | {self.get_supported_types()[1]}" + ) + print("Wrong File Type: ", type(error_message).__name__, str(error_message)) + return None + + def process_image(self): + print("---Processing Images---") + # Get Image Properties Type + get_params_type = self.comboBox.currentText() + + # Get Text boxes to process image + image_path = self.input_image_path.text() + output_dir = self.input_image_path_2.text() + rows = int(self.rows_box.value()) + columns = int(self.columns_box.value()) + gap = int(self.gap_box.value()) + + # Determine if the default or user defined values should be used + if get_params_type == "Defaults": + if not image_path: + image_path = "assets/Preview.png" + if not output_dir: + output_dir = os.path.join(os.path.expanduser("~"), "Desktop") + rows = 2 + columns = 6 + gap = 40 + # Get the text from the required textboxes + elif get_params_type == "User Defined": + if not image_path: + image_path = "assets/Preview.png" + if not output_dir: + output_dir = os.path.join(os.path.expanduser("~"), "Desktop") + + # Pass onto Determine_Split_Type function, that then passes it onto the appropriate Splitting funcion + self.determine_split_type(image_path, output_dir, rows, columns, gap) + + # Buttons + # Grabs the Path to an Image, and if possible adds this into a widgets textbox + def browse_image(self): + print("---Browsing for Image---") + + # Ask the user to select an Image + file_path = filedialog.askopenfilename( + filetypes=[("Image files", "*.jpg;*.jpeg;*.png;*.gif;*.bmp")] + ) + # Put File Path into textbox if widget has a textbox + if file_path: + self.input_image_path.setText(file_path) + self.process_preview() + # Just in case its ever needed + return file_path + + # Grabs the Path to a Directory, and if possible adds this into a widgets textbox + def browse_directory(self): + print("---Browsing for Output Dir---") + output_path = filedialog.askdirectory() + # Put Directory Path into textbox if widget has a textbox + if output_path: + self.input_image_path_2.setText(output_path) + # Just in case its ever needed + return output_path + + # Function that shows or hides the settings box according to the value of the ComboBox + def settings_visibilty(self, index): + if index == 0: + self.settings_wrapper.hide() + else: + self.settings_wrapper.show() + + +if __name__ == "__main__": + app = QApplication([]) + window = UI_MainWindow() + window.show() + app.exec() diff --git a/gui_source.ui b/ProjectFiles/gui_source.ui similarity index 89% rename from gui_source.ui rename to ProjectFiles/gui_source.ui index 5567660..6b219a7 100644 --- a/gui_source.ui +++ b/ProjectFiles/gui_source.ui @@ -136,7 +136,7 @@ border-bottom: 2px solid grey - path + Qt::AlignCenter @@ -205,7 +205,7 @@ border-bottom: 2px solid grey - path + Qt::AlignCenter @@ -313,7 +313,7 @@ QComboBox::drop-down { } QComboBox::down-arrow { - image: url(ProjectFiles/assets/chevron-down.svg) + image: url(assets/chevron-down.svg) } QComboBox:hover { @@ -344,7 +344,7 @@ QComboBox::drop-down:hover { - + 15 @@ -380,13 +380,13 @@ QSpinBox::down-button { } QSpinBox::up-arrow { - image: url(up_arrow.png); + image: url(assets/chevron-up.svg); width: 10px; height: 10px; } QSpinBox::down-arrow { - image: url(down_arrow.png); + image: url(assets/chevron-up.svg); width: 10px; height: 10px; } @@ -554,60 +554,7 @@ QSpinBox::down-arrow { 150 - - - - 16777215 - 16777215 - - - - - - - ProjectFiles/assets/Preview.png - - - false - - - - - - - - 0 - 40 - - - - - 15 - false - false - - - - PointingHandCursor - - - margin-left: 100px; -margin-right: 100px - - - Update Preview - - - - ProjectFiles/assets/refresh_icon.svgProjectFiles/assets/refresh_icon.svg - - - - 30 - 30 - - - +