import sys import os import datetime import subprocess import re import math import time from PyQt6.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, QFileDialog, QProgressBar, QTextEdit, QMessageBox, QCheckBox ) from PyQt6.QtCore import Qt, QTimer from PIL import Image class ImageSlicer(QWidget): def __init__(self): super().__init__() self.is_slicing = False self.progress_target = 0 self.progress_timer = QTimer(self) self.progress_timer.timeout.connect(self.update_progress_bar) self.initUI() def initUI(self): self.setWindowTitle('Image Slicer') # Layouts main_layout = QVBoxLayout() form_layout = QVBoxLayout() button_layout = QHBoxLayout() # Folder selection folder_layout = QHBoxLayout() self.folder_label = QLabel('Image Folder:') self.folder_path = QLineEdit() self.folder_button = QPushButton('Browse') self.folder_button.clicked.connect(self.browse_folder) folder_layout.addWidget(self.folder_label) folder_layout.addWidget(self.folder_path) folder_layout.addWidget(self.folder_button) form_layout.addLayout(folder_layout) # Dimensions dimensions_layout = QHBoxLayout() self.width_label = QLabel('Width (px):') self.width_input = QLineEdit('0') self.height_label = QLabel('Height (px):') self.height_input = QLineEdit() dimensions_layout.addWidget(self.width_label) dimensions_layout.addWidget(self.width_input) dimensions_layout.addWidget(self.height_label) dimensions_layout.addWidget(self.height_input) form_layout.addLayout(dimensions_layout) # Checkboxes checkbox_layout = QHBoxLayout() self.save_combined_checkbox = QCheckBox("Save Combined Image") self.save_combined_checkbox.setChecked(False) self.skip_merge_checkbox = QCheckBox("Skip Image Merging") self.skip_merge_checkbox.stateChanged.connect(self.update_save_combined_state) checkbox_layout.addWidget(self.save_combined_checkbox) checkbox_layout.addWidget(self.skip_merge_checkbox) form_layout.addLayout(checkbox_layout) # Progress bar self.progress_bar = QProgressBar() self.progress_bar.setStyleSheet(""" QProgressBar::chunk { background-color: orange; } """) form_layout.addWidget(self.progress_bar) # Log screen self.log_screen = QTextEdit() self.log_screen.setReadOnly(True) form_layout.addWidget(self.log_screen) # Action buttons self.start_button = QPushButton('Start') self.start_button.clicked.connect(self.toggle_slicing) self.save_log_button = QPushButton('Save Log') self.save_log_button.clicked.connect(self.save_log) button_layout.addWidget(self.start_button) button_layout.addWidget(self.save_log_button) main_layout.addLayout(form_layout) main_layout.addLayout(button_layout) self.setLayout(main_layout) def browse_folder(self): if self.is_slicing: return folder = QFileDialog.getExistingDirectory(self, 'Select Folder') if folder: self.folder_path.setText(folder) self.log(f"Selected folder: {folder}") def update_save_combined_state(self): is_checked = self.skip_merge_checkbox.isChecked() self.save_combined_checkbox.setEnabled(not is_checked) if is_checked: self.save_combined_checkbox.setChecked(False) def log(self, message): self.log_screen.append(message) QApplication.processEvents() def cm_to_pixels(self, cm): return int(cm * 96 / 2.54) def transform_folder_name(self, folder_name): # Example: C1_이 시국에 개인교습 의 06화 -> C1_이 시국에 개인교_06화 if ' 의 ' in folder_name: parts = folder_name.split(' 의 ') prefix_part = parts[0] episode_part = parts[1] # Remove '습' from the prefix part if it exists if prefix_part.endswith('습'): prefix_part = prefix_part[:-1] return f"{prefix_part}_{episode_part}" return folder_name # Return as is if ' 의 ' not found def create_combined_folder_name(self, root_folder_path, subfolder_path): """ Create a combined folder name using the root folder name and subfolder name """ root_folder_name = os.path.basename(root_folder_path) subfolder_name = os.path.basename(subfolder_path) combined_name = f"{root_folder_name}_{subfolder_name}" return combined_name def open_folder(self, path): if sys.platform == "win32": os.startfile(path) elif sys.platform == "darwin": subprocess.Popen(["open", path]) else: subprocess.Popen(["xdg-open", path]) def toggle_slicing(self, checked=False): if self.is_slicing: self.stop_slicing() else: self.start_slicing() def stop_slicing(self): self.is_slicing = False self.log("Stopping process...") def update_progress_bar(self): current_value = self.progress_bar.value() if current_value < self.progress_target: self.progress_bar.setValue(current_value + 1) elif current_value > self.progress_target: self.progress_bar.setValue(self.progress_target) def save_folder_paths_to_info_txt(self, folder_path): self.log("Finding all subdirectories with images...") self.progress_target = 20 folders_with_images = [] for root, dirs, files in os.walk(folder_path): QApplication.processEvents() # Keep UI responsive # Exclude _combined and _log folders if os.path.basename(root).endswith(('_combined', '_log')): continue if any(f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')) for f in files): folders_with_images.append(root) if not folders_with_images: self.log("No folders with images found.") self.progress_target = 100 return self.log(f"Found {len(folders_with_images)} folders. Saving to info.txt...") self.progress_target = 60 info_file_path = os.path.join(folder_path, "info.txt") try: with open(info_file_path, 'w', encoding='utf-8') as f: for folder in sorted(folders_with_images): f.write(f"{folder}\n") self.log(f"Successfully saved paths to info.txt (overwrote previous content)") except Exception as e: self.log(f"Error saving info.txt: {e}") self.progress_target = 100 def merge_and_slice_images(self, folder_path, slice_width_px, slice_height_px, save_combined): self.log("Finding folders to process...") self.progress_target = 5 QApplication.processEvents() folders_to_process = [] processed_result_folders = [] # New list to store result folder paths # Check if the root folder itself contains images if any(f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')) for f in os.listdir(folder_path)): folders_to_process.append(folder_path) # Find subfolders with images for item in os.listdir(folder_path): item_path = os.path.join(folder_path, item) if os.path.isdir(item_path): if any(f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')) for f in os.listdir(item_path)): folders_to_process.append(item_path) self.progress_target = 10 if not folders_to_process: self.log("No folders with images found to process.") self.progress_target = 100 return total_folders = len(folders_to_process) progress_per_folder = 90 / total_folders try: for i, current_folder in enumerate(folders_to_process): if not self.is_slicing: break base_progress = 10 + (i * progress_per_folder) self.log(f"--- Processing folder: {os.path.basename(current_folder)} ---") self.progress_target = int(base_progress) image_files = [os.path.join(current_folder, f) for f in os.listdir(current_folder) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif'))] def natural_sort_key(s): return [int(text) if text.isdigit() else text.lower() for text in re.split('([0-9]+)', s)] image_files.sort(key=natural_sort_key) if not image_files: continue self.log(f"Merging {len(image_files)} images...") self.progress_target = int(base_progress + (progress_per_folder * 0.2)) slice_height_px = slice_height_px target_width_px = slice_width_px if slice_width_px > 0 else 0 processed_images = [] first_image_width = 0 for image_path in image_files: if not self.is_slicing: break img = Image.open(image_path) if target_width_px == 0: if first_image_width == 0: first_image_width = img.width current_target_width = first_image_width else: current_target_width = target_width_px if img.width != current_target_width: aspect_ratio = img.height / img.width new_height = int(current_target_width * aspect_ratio) img = img.resize((current_target_width, new_height), Image.Resampling.LANCZOS) processed_images.append(img) QApplication.processEvents() if not self.is_slicing: break self.progress_target = int(base_progress + (progress_per_folder * 0.6)) total_height = sum(p_img.height for p_img in processed_images) combined_image = Image.new('RGB', (processed_images[0].width, total_height)) current_height = 0 for p_img in processed_images: combined_image.paste(p_img, (0, current_height)) current_height += p_img.height # Create combined folder name using root folder and subfolder names if current_folder == folder_path: # If processing the root folder itself result_folder_name = os.path.basename(folder_path) else: # If processing a subfolder result_folder_name = self.create_combined_folder_name(folder_path, current_folder) # Create output directory in the subfolder being processed output_dir = os.path.join(current_folder, result_folder_name) # Clear existing files in the output directory if os.path.exists(output_dir): for existing_file in os.listdir(output_dir): existing_file_path = os.path.join(output_dir, existing_file) if os.path.isfile(existing_file_path): os.remove(existing_file_path) self.log(f"Cleared existing files in {result_folder_name}") os.makedirs(output_dir, exist_ok=True) processed_result_folders.append(output_dir) self.log(f"Created output folder: {result_folder_name}") if save_combined: combined_folder_name = f"{result_folder_name}_combined" combined_dir = os.path.join(current_folder, combined_folder_name) # Clear existing files in the combined directory if os.path.exists(combined_dir): for existing_file in os.listdir(combined_dir): existing_file_path = os.path.join(combined_dir, existing_file) if os.path.isfile(existing_file_path): os.remove(existing_file_path) self.log(f"Cleared existing files in {combined_folder_name}") os.makedirs(combined_dir, exist_ok=True) combined_image_path = os.path.join(combined_dir, "combined_image.png") combined_image.save(combined_image_path, 'PNG') self.log(f"Saved combined image to: {combined_image_path}") num_slices = math.ceil(combined_image.height / slice_height_px) for j in range(num_slices): if not self.is_slicing: break top = j * slice_height_px bottom = min((j + 1) * slice_height_px, combined_image.height) box = (0, top, combined_image.width, bottom) if top >= bottom: continue cropped_img = combined_image.crop(box) # Create filename using the combined folder name + index slice_filename = f"{result_folder_name}_{j+1:02d}.jpg" slice_path = os.path.join(output_dir, slice_filename) cropped_img.save(slice_path, 'JPEG', quality=95) self.log(f"Saved {num_slices} slices in {result_folder_name} (format: {result_folder_name}_01.jpg, {result_folder_name}_02.jpg, ...)") self.progress_target = int(base_progress + progress_per_folder) # Save processed result folders to info.txt (overwrite previous content) if processed_result_folders: info_file_path = os.path.join(folder_path, "info.txt") try: with open(info_file_path, 'w', encoding='utf-8') as f: for result_folder in sorted(processed_result_folders): f.write(f"{result_folder}\n") self.log(f"Successfully saved processed folder paths to info.txt (overwrote previous content)") except Exception as e: self.log(f"Error saving info.txt for processed folders: {e}") except Exception as e: raise # Re-raise the exception to be caught by start_slicing def start_slicing(self): self.log_screen.clear() self.progress_bar.setValue(0) self.progress_target = 0 folder_path = self.folder_path.text() slice_width_cm_str = self.width_input.text() slice_height_cm_str = self.height_input.text() if not os.path.isdir(folder_path): QMessageBox.warning(self, "Warning", "Please select a valid folder.") self.log("Error: Invalid folder path.") return try: slice_width_px = int(slice_width_cm_str) if slice_width_cm_str else 0 slice_height_px = int(slice_height_cm_str) if slice_height_cm_str else 0 if slice_width_px < 0 or (not self.skip_merge_checkbox.isChecked() and slice_height_px <= 0): raise ValueError except (ValueError, TypeError): QMessageBox.warning(self, "Warning", "Please enter valid positive integers for height (width can be 0).") self.log("Error: Invalid input.") return self.is_slicing = True self.start_button.setText("Stop") self.progress_timer.start(10) save_combined = self.save_combined_checkbox.isChecked() skip_merge = self.skip_merge_checkbox.isChecked() try: if skip_merge: self.log("--- Skipping Merge: Saving folder paths to info.txt ---") self.save_folder_paths_to_info_txt(folder_path) else: self.log("--- Starting Image Merge and Slice Process ---") self.merge_and_slice_images(folder_path, slice_width_px, slice_height_px, save_combined) if self.is_slicing: self.progress_target = 100 while self.progress_bar.value() < 99: time.sleep(0.01) QApplication.processEvents() self.log("--- All tasks completed ---") QMessageBox.information(self, "Success", "Image processing completed for all tasks.") except Exception as e: self.log(f"An unexpected error occurred: {e}") QMessageBox.critical(self, "Error", f"An unexpected error occurred: {e}") finally: self.stop_slicing_ui() def stop_slicing_ui(self): self.is_slicing = False self.start_button.setText("Start") self.progress_timer.stop() self.progress_bar.setValue(0) def save_log(self): if self.is_slicing: QMessageBox.warning(self, "Warning", "Cannot save log while process is running.") return folder_path = self.folder_path.text() if not folder_path or not os.path.isdir(folder_path): QMessageBox.warning(self, "Warning", "Please select a valid base folder first.") return log_content = self.log_screen.toPlainText() if not log_content: QMessageBox.warning(self, "Warning", "Log is empty.") return try: log_dir_name = f"{os.path.basename(folder_path)}_log" log_dir_path = os.path.join(folder_path, log_dir_name) os.makedirs(log_dir_path, exist_ok=True) now = datetime.datetime.now() log_filename = f"log_{now.strftime('%H%M%S')}.log" save_path = os.path.join(log_dir_path, log_filename) with open(save_path, 'w', encoding='utf-8') as f: f.write(log_content) self.log(f"Log saved to {save_path}") QMessageBox.information(self, "Success", f"Log successfully saved to\n{save_path}") except Exception as e: self.log(f"Error saving log: {e}") QMessageBox.critical(self, "Error", f"Failed to save log file: {e}") if __name__ == '__main__': app = QApplication(sys.argv) ex = ImageSlicer() ex.show() sys.exit(app.exec())