commit 93e2f2477bffd567d2ac114eb44f947299c59990 Author: jeon-yeongjae Date: Fri Jul 18 18:25:05 2025 +0700 image cutting -> indesign script diff --git a/image_cutting/backup/main_완성.py b/image_cutting/backup/main_완성.py new file mode 100644 index 0000000..c46b7c0 --- /dev/null +++ b/image_cutting/backup/main_완성.py @@ -0,0 +1,370 @@ +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 (cm):') + self.width_input = QLineEdit('0') + self.height_label = QLabel('Height (cm):') + 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") + 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 log(self, message): + self.log_screen.append(message) + QApplication.processEvents() + + def cm_to_pixels(self, cm): + return int(cm * 96 / 2.54) + + 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_file_path}") + 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_cm, slice_height_cm, 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 + if any(f.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')) for f in os.listdir(folder_path)): + folders_to_process.append(folder_path) + 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 = self.cm_to_pixels(slice_height_cm) + target_width_px = self.cm_to_pixels(slice_width_cm) if slice_width_cm > 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 + + result_folder_name = f"{os.path.basename(current_folder)}_result" + output_dir = os.path.join(current_folder, result_folder_name) + os.makedirs(output_dir, exist_ok=True) + processed_result_folders.append(output_dir) # Add to list + + if save_combined: + combined_folder_name = f"{os.path.basename(current_folder)}_combined" + combined_dir = os.path.join(os.path.dirname(output_dir), 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) + slice_filename = f"result_{j+1}.png" + slice_path = os.path.join(output_dir, slice_filename) + cropped_img.save(slice_path, 'PNG') + self.log(f"Saved {num_slices} slices in {result_folder_name}") + self.progress_target = int(base_progress + progress_per_folder) + + # Save processed result folders to info.txt + 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_file_path}") + 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_cm = float(slice_width_cm_str) if slice_width_cm_str else 0 + slice_height_cm = float(slice_height_cm_str) if slice_height_cm_str else 0 + if slice_width_cm < 0 or (not self.skip_merge_checkbox.isChecked() and slice_height_cm <= 0): + raise ValueError + except (ValueError, TypeError): + QMessageBox.warning(self, "Warning", "Please enter valid positive numbers 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_cm, slice_height_cm, 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()) \ No newline at end of file diff --git a/image_cutting/backup/main_완성_주석없이.py b/image_cutting/backup/main_완성_주석없이.py new file mode 100644 index 0000000..c8e148b --- /dev/null +++ b/image_cutting/backup/main_완성_주석없이.py @@ -0,0 +1,435 @@ +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()) \ No newline at end of file diff --git a/image_cutting/main.py b/image_cutting/main.py new file mode 100644 index 0000000..befe5e3 --- /dev/null +++ b/image_cutting/main.py @@ -0,0 +1,770 @@ +""" +Image Slicer Application + +This PyQt6-based application merges and slices images from folders. +It can process multiple folders containing images, merge them vertically, +and slice them into specified dimensions. + +Features: +- Browse and select image folders +- Set custom width and height for slices (in pixels) +- Option to save combined images before slicing +- Option to skip merging and just scan folders +- Progress tracking with visual progress bar +- Comprehensive logging system +- Save logs to files + +Author: [Your Name] +Date: [Date] +Version: 1.0 +""" + +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): + """ + Main application class for the Image Slicer GUI. + + This class handles the user interface and coordinates all image processing operations. + It provides functionality to merge multiple images vertically and slice them into + smaller pieces based on user-defined dimensions. + """ + + def __init__(self): + """ + Initialize the ImageSlicer application. + + Sets up initial state variables and UI components. + """ + super().__init__() + + # Processing state variables + self.is_slicing = False # Flag to track if slicing operation is in progress + self.progress_target = 0 # Target progress value for smooth progress bar animation + + # Timer for smooth progress bar updates + self.progress_timer = QTimer(self) + self.progress_timer.timeout.connect(self.update_progress_bar) + + # Initialize the user interface + self.initUI() + + def initUI(self): + """ + Initialize and set up the user interface components. + + Creates all UI elements including: + - Folder selection controls + - Dimension input fields + - Checkboxes for options + - Progress bar + - Log display area + - Action buttons + """ + self.setWindowTitle('Image Slicer') + + # Main layout containers + main_layout = QVBoxLayout() # Primary vertical layout + form_layout = QVBoxLayout() # Layout for form elements + button_layout = QHBoxLayout() # Layout for action buttons + + # === FOLDER SELECTION SECTION === + folder_layout = QHBoxLayout() + self.folder_label = QLabel('Image Folder:') + self.folder_path = QLineEdit() # Text field to display selected folder path + self.folder_button = QPushButton('Browse') + self.folder_button.clicked.connect(self.browse_folder) + + # Add folder selection components to layout + folder_layout.addWidget(self.folder_label) + folder_layout.addWidget(self.folder_path) + folder_layout.addWidget(self.folder_button) + form_layout.addLayout(folder_layout) + + # === DIMENSION INPUT SECTION === + dimensions_layout = QHBoxLayout() + self.width_label = QLabel('Width (px):') + self.width_input = QLineEdit('0') # Default width of 0 means auto-detect + self.height_label = QLabel('Height (px):') + self.height_input = QLineEdit() # Height must be specified by user + + # Add dimension input components to layout + 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) + + # === OPTIONS CHECKBOXES SECTION === + checkbox_layout = QHBoxLayout() + + # Checkbox to save the combined image before slicing + self.save_combined_checkbox = QCheckBox("Save Combined Image") + self.save_combined_checkbox.setChecked(False) + + # Checkbox to skip image merging and just scan folders + self.skip_merge_checkbox = QCheckBox("Skip Image Merging") + self.skip_merge_checkbox.stateChanged.connect(self.update_save_combined_state) + + # Add checkboxes to layout + checkbox_layout.addWidget(self.save_combined_checkbox) + checkbox_layout.addWidget(self.skip_merge_checkbox) + form_layout.addLayout(checkbox_layout) + + # === PROGRESS BAR SECTION === + self.progress_bar = QProgressBar() + # Custom styling for orange progress bar + self.progress_bar.setStyleSheet(""" + QProgressBar::chunk { + background-color: orange; + } + """) + form_layout.addWidget(self.progress_bar) + + # === LOG DISPLAY SECTION === + self.log_screen = QTextEdit() + self.log_screen.setReadOnly(True) # Make log area read-only + form_layout.addWidget(self.log_screen) + + # === ACTION BUTTONS SECTION === + 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) + + # Add buttons to layout + button_layout.addWidget(self.start_button) + button_layout.addWidget(self.save_log_button) + + # === ASSEMBLE MAIN LAYOUT === + main_layout.addLayout(form_layout) + main_layout.addLayout(button_layout) + self.setLayout(main_layout) + + def browse_folder(self): + """ + Open a folder selection dialog and update the folder path field. + + This method is called when the 'Browse' button is clicked. + It prevents folder selection during active processing operations. + """ + # Prevent folder selection during processing + if self.is_slicing: + return + + # Open folder selection dialog + folder = QFileDialog.getExistingDirectory(self, 'Select Folder') + + if folder: + # Update the folder path field and log the selection + self.folder_path.setText(folder) + self.log(f"Selected folder: {folder}") + + def update_save_combined_state(self): + """ + Update the state of the 'Save Combined Image' checkbox. + + When 'Skip Image Merging' is checked, the 'Save Combined Image' option + becomes disabled since there's no combined image to save. + """ + is_checked = self.skip_merge_checkbox.isChecked() + + # Disable/enable the save combined checkbox based on skip merge state + self.save_combined_checkbox.setEnabled(not is_checked) + + # Uncheck save combined if skip merge is enabled + if is_checked: + self.save_combined_checkbox.setChecked(False) + + def log(self, message): + """ + Add a message to the log display area. + + Args: + message (str): The message to log + + This method also processes Qt events to keep the UI responsive + during long-running operations. + """ + self.log_screen.append(message) + QApplication.processEvents() # Keep UI responsive during logging + + def cm_to_pixels(self, cm): + """ + Convert centimeters to pixels using standard DPI conversion. + + Args: + cm (float): Length in centimeters + + Returns: + int: Equivalent length in pixels (96 DPI standard) + + Note: This function is kept for potential future use but is not + currently used since the application now works with pixels directly. + """ + return int(cm * 96 / 2.54) + + def transform_folder_name(self, folder_name): + """ + Transform folder names with Korean naming patterns. + + Args: + folder_name (str): Original folder name + + Returns: + str: Transformed folder name + + Example: + Input: "C1_이 시국에 개인교습 의 06화" + Output: "C1_이 시국에 개인교_06화" + + Note: This function handles specific Korean manga/webtoon naming conventions. + """ + # Check for Korean naming pattern with ' 의 ' separator + if ' 의 ' in folder_name: + parts = folder_name.split(' 의 ') + prefix_part = parts[0] + episode_part = parts[1] + + # Remove trailing '습' character if present + if prefix_part.endswith('습'): + prefix_part = prefix_part[:-1] + + return f"{prefix_part}_{episode_part}" + + # Return unchanged if pattern not found + return folder_name + + def create_combined_folder_name(self, root_folder_path, subfolder_path): + """ + Create a combined folder name using root and subfolder names. + + Args: + root_folder_path (str): Path to the root folder (user-selected) + subfolder_path (str): Path to the subfolder being processed + + Returns: + str: Combined folder name in format "RootName_SubfolderName" + + Example: + Root: "/Users/MyImages" + Subfolder: "/Users/MyImages/Chapter1" + Result: "MyImages_Chapter1" + """ + 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): + """ + Open a folder in the system's default file manager. + + Args: + path (str): Path to the folder to open + + This method handles cross-platform folder opening: + - Windows: uses os.startfile() + - macOS: uses 'open' command + - Linux: uses 'xdg-open' command + """ + 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): + """ + Toggle between starting and stopping the slicing process. + + Args: + checked (bool): Unused parameter from signal connection + + This method serves as the main controller for the Start/Stop button. + """ + if self.is_slicing: + self.stop_slicing() + else: + self.start_slicing() + + def stop_slicing(self): + """ + Stop the current slicing operation. + + Sets the slicing flag to False, which causes the processing loops + to exit gracefully at their next iteration. + """ + self.is_slicing = False + self.log("Stopping process...") + + def update_progress_bar(self): + """ + Update the progress bar value smoothly towards the target. + + This method is called by a timer to create smooth progress bar animations. + It gradually moves the current value towards the target value. + """ + current_value = self.progress_bar.value() + + if current_value < self.progress_target: + # Increment progress bar value + self.progress_bar.setValue(current_value + 1) + elif current_value > self.progress_target: + # Jump directly to target if we've overshot + self.progress_bar.setValue(self.progress_target) + + def save_folder_paths_to_info_txt(self, folder_path): + """ + Scan for folders containing images and save their paths to info.txt. + + Args: + folder_path (str): Root folder path to scan + + This method is used when "Skip Image Merging" is enabled. + It finds all subfolders containing image files and saves their + paths to an info.txt file for reference. + """ + self.log("Finding all subdirectories with images...") + self.progress_target = 20 + + folders_with_images = [] + + # Walk through all subdirectories + for root, dirs, files in os.walk(folder_path): + QApplication.processEvents() # Keep UI responsive during scanning + + # Skip folders that are processing results + if os.path.basename(root).endswith(('_combined', '_log')): + continue + + # Check if folder contains image files + image_extensions = ('.png', '.jpg', '.jpeg', '.bmp', '.gif') + if any(f.lower().endswith(image_extensions) for f in files): + folders_with_images.append(root) + + # Handle case where no image folders are found + 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 + + # Write folder paths to info.txt + 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): + """ + Main image processing method that merges and slices images. + + Args: + folder_path (str): Root folder containing image folders + slice_width_px (int): Target width for slices in pixels (0 = auto) + slice_height_px (int): Target height for slices in pixels + save_combined (bool): Whether to save the combined image + + This method: + 1. Finds all folders containing images + 2. For each folder, merges all images vertically + 3. Slices the combined image into pieces of specified dimensions + 4. Saves results with combined folder names + """ + self.log("Finding folders to process...") + self.progress_target = 5 + QApplication.processEvents() + + folders_to_process = [] + processed_result_folders = [] # Track processed folders for info.txt + + # Check if the root folder itself contains images + image_extensions = ('.png', '.jpg', '.jpeg', '.bmp', '.gif') + if any(f.lower().endswith(image_extensions) for f in os.listdir(folder_path)): + folders_to_process.append(folder_path) + + # Find subfolders containing 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(image_extensions) for f in os.listdir(item_path)): + folders_to_process.append(item_path) + + self.progress_target = 10 + + # Exit if no folders with images found + if not folders_to_process: + self.log("No folders with images found to process.") + self.progress_target = 100 + return + + # Calculate progress increment per folder + total_folders = len(folders_to_process) + progress_per_folder = 90 / total_folders + + try: + # Process each folder containing images + for i, current_folder in enumerate(folders_to_process): + # Check if user requested to stop + if not self.is_slicing: + break + + # Calculate base progress for this folder + base_progress = 10 + (i * progress_per_folder) + self.log(f"--- Processing folder: {os.path.basename(current_folder)} ---") + self.progress_target = int(base_progress) + + # === STEP 1: GATHER AND SORT IMAGE FILES === + image_files = [ + os.path.join(current_folder, f) + for f in os.listdir(current_folder) + if f.lower().endswith(image_extensions) + ] + + # Sort files naturally with proper zero-padding handling + def natural_sort_key(s): + """ + Enhanced natural sorting that properly handles zero-padded numbers. + + Example: + - "file_01.jpg" comes before "file_1.jpg" + - "file_1.jpg" comes before "file_2.jpg" + - "file_2.jpg" comes before "file_10.jpg" + """ + parts = re.split('([0-9]+)', s) + result = [] + for part in parts: + if part.isdigit(): + # For numeric parts, use (numeric_value, original_string, length) + # This ensures proper ordering: 01 < 1 < 02 < 2 < 10 + numeric_value = int(part) + original_length = len(part) + result.append((0, numeric_value, original_length, part)) + else: + # For text parts, use simple string comparison + result.append((1, part.lower(), 0, part)) + return result + image_files.sort(key=natural_sort_key) + + # Skip folder if no images found + if not image_files: + continue + + self.log(f"Merging {len(image_files)} images...") + self.progress_target = int(base_progress + (progress_per_folder * 0.2)) + + # === STEP 2: SET UP SLICE DIMENSIONS === + slice_height_px = slice_height_px # Height for each slice + target_width_px = slice_width_px if slice_width_px > 0 else 0 # 0 = auto-detect + + # === STEP 3: PROCESS AND RESIZE IMAGES === + processed_images = [] + first_image_width = 0 # Store width of first image for auto-detection + + for image_path in image_files: + # Check if user requested to stop + if not self.is_slicing: + break + + # Load image + img = Image.open(image_path) + + # Determine target width + if target_width_px == 0: + # Auto-detect width from first image + if first_image_width == 0: + first_image_width = img.width + current_target_width = first_image_width + else: + # Use specified width + current_target_width = target_width_px + + # Resize image if width doesn't match target + 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() # Keep UI responsive + + # Exit if user stopped during processing + if not self.is_slicing: + break + + self.progress_target = int(base_progress + (progress_per_folder * 0.6)) + + # === STEP 4: MERGE IMAGES VERTICALLY === + 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: + # Paste each image below the previous one + combined_image.paste(p_img, (0, current_height)) + current_height += p_img.height + + # === STEP 5: CREATE OUTPUT FOLDER NAME === + if current_folder == folder_path: + # Processing root folder itself + result_folder_name = os.path.basename(folder_path) + else: + # Processing subfolder - combine root and subfolder names + result_folder_name = self.create_combined_folder_name(folder_path, current_folder) + + # === STEP 6: CREATE OUTPUT DIRECTORY === + 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}") + + # === STEP 7: SAVE COMBINED IMAGE (OPTIONAL) === + 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}") + + # === STEP 8: SLICE COMBINED IMAGE === + num_slices = math.ceil(combined_image.height / slice_height_px) + + for j in range(num_slices): + # Check if user requested to stop + if not self.is_slicing: + break + + # Calculate slice boundaries + top = j * slice_height_px + bottom = min((j + 1) * slice_height_px, combined_image.height) + box = (0, top, combined_image.width, bottom) + + # Skip empty slices + if top >= bottom: + continue + + # Crop the slice from combined image + cropped_img = combined_image.crop(box) + + # Create filename with zero-padded index + slice_filename = f"{result_folder_name}_{j+1:02d}.jpg" + slice_path = os.path.join(output_dir, slice_filename) + + # Save slice as JPEG with high quality + cropped_img.save(slice_path, 'JPEG', quality=95) + + self.log(f"Saved {num_slices} slices in {result_folder_name} " + f"(format: {result_folder_name}_01.jpg, {result_folder_name}_02.jpg, ...)") + self.progress_target = int(base_progress + progress_per_folder) + + # === STEP 9: SAVE PROCESSED FOLDERS TO INFO.TXT === + 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: + # Re-raise exception to be handled by start_slicing method + raise + + def start_slicing(self): + """ + Start the image processing operation. + + This method: + 1. Validates user inputs + 2. Sets up the processing environment + 3. Chooses between folder scanning or image processing + 4. Handles errors and cleanup + """ + # === INITIALIZE PROCESSING ENVIRONMENT === + self.log_screen.clear() # Clear previous log messages + self.progress_bar.setValue(0) # Reset progress bar + self.progress_target = 0 + + # Get user inputs + folder_path = self.folder_path.text() + slice_width_px_str = self.width_input.text() + slice_height_px_str = self.height_input.text() + + # === VALIDATE FOLDER PATH === + if not os.path.isdir(folder_path): + QMessageBox.warning(self, "Warning", "Please select a valid folder.") + self.log("Error: Invalid folder path.") + return + + # === VALIDATE DIMENSION INPUTS === + try: + # Convert inputs to integers (pixels must be whole numbers) + slice_width_px = int(slice_width_px_str) if slice_width_px_str else 0 + slice_height_px = int(slice_height_px_str) if slice_height_px_str else 0 + + # Validate values (width can be 0 for auto-detect, height must be positive) + 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 + + # === START PROCESSING === + self.is_slicing = True # Set processing flag + self.start_button.setText("Stop") # Change button text + self.progress_timer.start(10) # Start progress bar animation timer + + # Get processing options + save_combined = self.save_combined_checkbox.isChecked() + skip_merge = self.skip_merge_checkbox.isChecked() + + try: + if skip_merge: + # === FOLDER SCANNING MODE === + self.log("--- Skipping Merge: Saving folder paths to info.txt ---") + self.save_folder_paths_to_info_txt(folder_path) + else: + # === IMAGE PROCESSING MODE === + self.log("--- Starting Image Merge and Slice Process ---") + self.merge_and_slice_images(folder_path, slice_width_px, slice_height_px, save_combined) + + # === COMPLETION HANDLING === + if self.is_slicing: # Check if not stopped by user + # Animate progress bar to 100% + 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: + # === ERROR HANDLING === + self.log(f"An unexpected error occurred: {e}") + QMessageBox.critical(self, "Error", f"An unexpected error occurred: {e}") + + finally: + # === CLEANUP === + self.stop_slicing_ui() + + def stop_slicing_ui(self): + """ + Reset the UI to non-processing state. + + This method is called when processing completes (successfully or with errors) + or when the user manually stops the operation. + """ + self.is_slicing = False # Clear processing flag + self.start_button.setText("Start") # Reset button text + self.progress_timer.stop() # Stop progress bar animation + self.progress_bar.setValue(0) # Reset progress bar + + def save_log(self): + """ + Save the current log content to a timestamped file. + + This method creates a log folder within the selected base folder + and saves the log with a timestamp in the filename. + """ + # Prevent log saving during processing + if self.is_slicing: + QMessageBox.warning(self, "Warning", "Cannot save log while process is running.") + return + + # Validate base folder selection + 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 + + # Check if log has content + log_content = self.log_screen.toPlainText() + if not log_content: + QMessageBox.warning(self, "Warning", "Log is empty.") + return + + try: + # === CREATE LOG DIRECTORY === + 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) + + # === GENERATE TIMESTAMPED FILENAME === + now = datetime.datetime.now() + log_filename = f"log_{now.strftime('%H%M%S')}.log" # Format: log_HHMMSS.log + save_path = os.path.join(log_dir_path, log_filename) + + # === SAVE LOG FILE === + with open(save_path, 'w', encoding='utf-8') as f: + f.write(log_content) + + # === CONFIRM SUCCESS === + self.log(f"Log saved to {save_path}") + QMessageBox.information(self, "Success", f"Log successfully saved to\n{save_path}") + + except Exception as e: + # === HANDLE SAVE ERRORS === + self.log(f"Error saving log: {e}") + QMessageBox.critical(self, "Error", f"Failed to save log file: {e}") + + +if __name__ == '__main__': + """ + Application entry point. + + Creates the QApplication instance, initializes the ImageSlicer widget, + shows the main window, and starts the event loop. + """ + app = QApplication(sys.argv) # Create Qt application + ex = ImageSlicer() # Create main window instance + ex.show() # Show the window + sys.exit(app.exec()) # Start event loop and exit when closed \ No newline at end of file diff --git a/indesign_script/image_to_epub.jsx b/indesign_script/image_to_epub.jsx new file mode 100755 index 0000000..37165b8 --- /dev/null +++ b/indesign_script/image_to_epub.jsx @@ -0,0 +1,467 @@ +// InDesign 이미지 처리 및 EPUB 내보내기 자동화 스크립트 +// 설정 파일에서 경로를 읽고, 이미지 파일들을 스캔하여 InDesign 문서 생성 후 EPUB으로 내보내기 + +CONFIG_FILE_PATH = "/Users/jeon-yeongjae/dev/info.txt" +EPUB_FOLDER_NAME = "epub" +LOG_FOLDER_NAME = "log" + +// 설정 파일에서 루트 경로 읽기 +function getRootPaths() { + try { + var configFile = new File(CONFIG_FILE_PATH); + var paths = []; + + if (configFile.exists) { + configFile.open("r"); + + while (!configFile.eof) { + var line = configFile.readln(); + var cleanedPath = line.replace(/\r\n|\r|\n/g, "").replace(/^\s+|\s+$/g, ""); + + if (cleanedPath) { + paths.push(cleanedPath); + } + } + configFile.close(); + + return paths; + } else { + throw new Error("Config file not found: " + CONFIG_FILE_PATH); + } + } catch (error) { + throw new Error("Error reading config file: " + error.message); + } +} + +// 단위 변환 함수들 +function mmToPoints(mm) { + return mm * 2.834645669; +} + +function pointsToMM(points) { + return points / 2.834645669; +} + +// 실행 시간 포맷팅 +function formatElapsedTime(milliseconds) { + var seconds = milliseconds / 1000; + + if (seconds < 60) { + return seconds.toFixed(1) + "s"; + } else { + var minutes = Math.floor(seconds / 60); + var remainingSeconds = (seconds % 60); + return minutes + "m " + remainingSeconds.toFixed(1) + "s"; + } +} + +// EPUB 내보내기 실행 (DPI 300 설정 추가) +// EPUB 내보내기 실행 (DPI 300 설정 추가) +function exportToEPUB(root_path, document, logMessages) { + try { + logMessages.push("\n=== Starting EPUB Export ==="); + + // 내보내기 폴더 생성 + var exportFolder = new Folder(root_path + "/" + EPUB_FOLDER_NAME); + if (!exportFolder.exists) exportFolder.create(); + + // 타임스탬프로 고유 파일명 생성 + var now = new Date(); + var timestamp = now.getFullYear() + "-" + + (now.getMonth() + 1).toString().replace(/^(\d)$/, "0$1") + "-" + + now.getDate().toString().replace(/^(\d)$/, "0$1") + "_" + + now.getHours().toString().replace(/^(\d)$/, "0$1") + "-" + + now.getMinutes().toString().replace(/^(\d)$/, "0$1") + "-" + + now.getSeconds().toString().replace(/^(\d)$/, "0$1"); + + var epubFileName = "ImageBook_" + timestamp + ".epub"; + var epubFile = new File(exportFolder.fsName + "/" + epubFileName); + + // EPUB 내보내기 설정 + try { + var epubPrefs = document.epubFixedLayoutExportPreferences; + // DPI 300 설정 + epubPrefs.imageExportResolution = ImageResolution.PPI_300 + logMessages.push("Image resolution set to 300 DPI"); + + epubPrefs.epubNavigationStyles = EpubNavigationStyle.FILENAME_NAVIGATION; // 파일명 기반 네비게이션 + logMessages.push("FILENAME_NAVIGATION resolution set to 300 DPI"); + // 추가 이미지 품질 설정 + // epubPrefs.imageConversion = ImageConversion.JPEG; + // epubPrefs.jpegOptionsQuality = JPEGOptionsQuality.HIGH; + // epubPrefs.imageAlignment = ImageAlignmentType.LEFT; + // epubPrefs.imageSpaceBefore = 0; + // epubPrefs.imageSpaceAfter = 0; + + // TOC Navigation 설정 (파일명 기반) + // epubPrefs.generateTOC = true; // TOC 생성 활성화 + // epubPrefs.includeDocumentTOC = false; // 문서 내 TOC 포함하지 않음 + // epubPrefs.tocStyleName = "Heading 1"; // 생성된 Heading 1 스타일 사용 + // epubPrefs.navigationTOCStyle = TOCStyleType.EPUB_TOC_STYLE; // Navigation TOC 스타일 + // epubPrefs.tocTitle = "Contents"; // TOC 제목 + // epubPrefs.tocDepth = 1; // TOC 깊이 (Heading 1만 사용) + + // 고정 레이아웃 관련 설정 + // epubPrefs.epubVersion = EPubVersion.EPUB3; + // epubPrefs.includeDocumentMetadata = true; + // epubPrefs.preserveLocalOverrides = true; + + + } catch (e) { + logMessages.push("Warning: Could not access all EPUB preferences: " + e.message); + logMessages.push("Proceeding with default settings..."); + } + + // 고정 레이아웃 EPUB으로 내보내기 (대화상자 없이, 자동 열기 없이) + document.exportFile(ExportFormat.FIXED_LAYOUT_EPUB, epubFile, false); + + // 내보내기 완료 대기 + $.sleep(3000); + + // 내보내기 성공 확인 + if (epubFile.exists) { + var fileSizeKB = Math.round(epubFile.length / 1024); + logMessages.push("EPUB export successful: " + epubFile.fsName); + logMessages.push("File size: " + fileSizeKB + " KB"); + logMessages.push("EPUB file saved but NOT automatically opened"); + return { success: true, filePath: epubFile.fsName }; + } else { + return { success: false, error: "EPUB not created" }; + } + } catch (error) { + return { success: false, error: error.message }; + } +} + +// 로그 파일 작성 +function writeLog(root_path, message) { + try { + var logFolder = new Folder(root_path + "/" + LOG_FOLDER_NAME); + if (!logFolder.exists) { + logFolder.create(); + } + + // 타임스탬프로 고유 로그 파일명 생성 + var now = new Date(); + var timestamp = now.getFullYear() + "-" + + (now.getMonth() + 1).toString().replace(/^(\d)$/, "0$1") + "-" + + now.getDate().toString().replace(/^(\d)$/, "0$1") + "_" + + now.getHours().toString().replace(/^(\d)$/, "0$1") + "-" + + now.getMinutes().toString().replace(/^(\d)$/, "0$1") + "-" + + now.getSeconds().toString().replace(/^(\d)$/, "0$1"); + + var logFile = new File(logFolder.fsName + "/indesign_script_" + timestamp + ".log"); + logFile.encoding = "UTF-8"; + logFile.open("w"); + logFile.write(message); + logFile.close(); + } catch (e) { + // 로그 작성 실패 시 스크립트 중단 방지 + } +} + +// 폴더에서 이미지 파일 검색 +function getAllImageFiles(folder, imageFiles, logMessages) { + if (!folder.exists) return; + + var supportedExtensions = /\.(jpg|jpeg|png|tif|tiff|psd|ai|eps|gif|bmp)$/i; + var files = folder.getFiles(); + + for (var i = 0; i < files.length; i++) { + var file = files[i]; + if (file instanceof Folder) { + // 하위 폴더 검색은 주석 처리됨 + } else if (file instanceof File && supportedExtensions.test(file.name)) { + imageFiles.push(file); + logMessages.push("Found image: " + file.name); + } + } +} + +// 파일명에서 언더스코어 뒤 숫자 추출 (정렬용) - 수정된 버전 +function extractNumberFromFilename(filename) { + // _숫자.확장자 패턴에서 숫자 추출 + var match = filename.match(/_(\d+)\./); + return match ? parseInt(match[1], 10) : 999999; +} + +// 파일명의 숫자 순서로 이미지 정렬 +function sortImagesByNumber(imageFiles) { + return imageFiles.sort(function(a, b) { + var numA = extractNumberFromFilename(a.name); + var numB = extractNumberFromFilename(b.name); + return numA - numB; + }); +} + +// 이미지 파일의 실제 크기 측정 +function getImageDimensions(imageFile, tempPage) { + try { + var tempFrame = tempPage.rectangles.add(); + tempFrame.place(imageFile); + + if (tempFrame.allGraphics.length > 0) { + var imageContent = tempFrame.allGraphics[0]; + var bounds = imageContent.geometricBounds; + var w = bounds[3] - bounds[1]; // 너비 = x2 - x1 + var h = bounds[2] - bounds[0]; // 높이 = y2 - y1 + tempFrame.remove(); + return { width: w, height: h }; + } else { + tempFrame.remove(); + return null; + } + } catch (error) { + try { tempFrame.remove(); } catch (e) {} + return null; + } +} + +// 메인 실행 부분 +var root_paths = getRootPaths(); + +for (var kk = 0; kk < root_paths.length; kk++) { + var root_path = root_paths[kk]; + var workFolder = new Folder(root_path); + var logMessages = []; + var mainDoc = null; + + // 시간 측정 변수 + var scriptStartTime = new Date(); + var sectionStartTime; + + try { + // 초기화 + sectionStartTime = new Date(); + logMessages.push("=== InDesign Script Start ==="); + logMessages.push("Script start time: " + scriptStartTime.toString()); + logMessages.push("Root path: " + root_path); + + if (!workFolder.exists) { + logMessages.push("ERROR: Work folder not found: " + workFolder.fsName); + writeLog(root_path, logMessages.join("\n")); + continue; + } + + var initTime = new Date() - sectionStartTime; + logMessages.push("✓ Initialization completed in " + formatElapsedTime(initTime)); + + // 이미지 파일 검색 + sectionStartTime = new Date(); + logMessages.push("\n=== Starting Image Discovery ==="); + + var imageFiles = []; + getAllImageFiles(workFolder, imageFiles, logMessages); + + if (imageFiles.length === 0) { + logMessages.push("ERROR: No images found"); + writeLog(root_path, logMessages.join("\n")); + continue; + } + + // 파일명 숫자 순서로 정렬 + imageFiles = sortImagesByNumber(imageFiles); + + logMessages.push("Images sorted by number in filename:"); + for (var j = 0; j < imageFiles.length; j++) { + logMessages.push(" " + (j + 1) + ". " + imageFiles[j].name + " (number: " + extractNumberFromFilename(imageFiles[j].name) + ")"); + } + + var discoveryTime = new Date() - sectionStartTime; + logMessages.push("✓ Image discovery completed in " + formatElapsedTime(discoveryTime) + " - Found " + imageFiles.length + " images"); + + // InDesign 문서 생성 + sectionStartTime = new Date(); + logMessages.push("\n=== Creating InDesign Document ==="); + + mainDoc = app.documents.add(); + mainDoc.documentPreferences.facingPages = false; // 단면 레이아웃 + + // 모든 여백을 0으로 설정 (전체 페이지 이미지용) + mainDoc.marginPreferences.top = 0; + mainDoc.marginPreferences.bottom = 0; + mainDoc.marginPreferences.left = 0; + mainDoc.marginPreferences.right = 0; + + // 가이드와 격자 숨김 + mainDoc.guidePreferences.guidesInBack = true; + mainDoc.guidePreferences.guidesShown = false; + mainDoc.gridPreferences.documentGridShown = false; + mainDoc.gridPreferences.baselineGridShown = false; + + // 프레임 가장자리 숨김 + try { + app.activeWindow.viewPreferences.showFrameEdges = false; + app.activeWindow.viewPreferences.showRulers = false; + } catch (e) { + logMessages.push("Note: Could not hide frame edges - will be visible in InDesign but not in export"); + } + + logMessages.push("Created new document with zero margins and hidden guides"); + + var docCreationTime = new Date() - sectionStartTime; + logMessages.push("✓ Document creation completed in " + formatElapsedTime(docCreationTime)); + + // 문서 크기 설정 + sectionStartTime = new Date(); + logMessages.push("\n=== Sizing Document ==="); + + var documentWidth, documentHeight; + + // 첫 번째 이미지 크기에 맞춰 문서 크기 설정 + if (imageFiles.length > 0) { + var firstImageFile = imageFiles[0]; + var tempPage = mainDoc.pages.item(0); + var firstImageDimensions = getImageDimensions(firstImageFile, tempPage); + + if (firstImageDimensions) { + documentWidth = firstImageDimensions.width; + documentHeight = firstImageDimensions.height; + + mainDoc.documentPreferences.pageWidth = documentWidth; + mainDoc.documentPreferences.pageHeight = documentHeight; + + logMessages.push("Document size set to first image: " + pointsToMM(documentWidth).toFixed(1) + "mm x " + pointsToMM(documentHeight).toFixed(1) + "mm"); + } else { + logMessages.push("ERROR: Could not measure first image, using default size"); + documentWidth = mainDoc.documentPreferences.pageWidth; + documentHeight = mainDoc.documentPreferences.pageHeight; + } + } + + var sizingTime = new Date() - sectionStartTime; + logMessages.push("✓ Document sizing completed in " + formatElapsedTime(sizingTime)); + + // 이미지 배치 처리 + sectionStartTime = new Date(); + logMessages.push("\n=== Processing Images ==="); + + for (var i = 0; i < imageFiles.length; i++) { + var imageStartTime = new Date(); + var imageFile = imageFiles[i]; + logMessages.push("\nProcessing: " + imageFile.name + " (Image " + (i + 1) + "/" + imageFiles.length + ")"); + + // 페이지 가져오기 또는 생성 + var currentPage; + if (i === 0) { + currentPage = mainDoc.pages.item(0); + } else { + currentPage = mainDoc.pages.add(); + logMessages.push("Added page " + (i + 1) + " with document size: " + pointsToMM(documentWidth).toFixed(1) + "mm x " + pointsToMM(documentHeight).toFixed(1) + "mm"); + } + + // 전체 페이지를 채우는 이미지 프레임 생성 + var imageFrame = currentPage.rectangles.add(); + imageFrame.geometricBounds = [0, 0, documentHeight, documentWidth]; + + // 이미지 배치 및 맞춤 + try { + imageFrame.place(imageFile); + if (imageFrame.allGraphics.length > 0) { + var img = imageFrame.allGraphics[0]; + img.fit(FitOptions.FILL_PROPORTIONALLY); // 비율 유지하며 채우기 + img.fit(FitOptions.CENTER_CONTENT); // 내용 중앙 정렬 + + var imageTime = new Date() - imageStartTime; + logMessages.push("✓ Image successfully placed on page " + (i + 1) + " (full page, no margins) - " + formatElapsedTime(imageTime)); + } else { + logMessages.push("ERROR: Could not place image content: " + imageFile.name); + } + } catch (placeError) { + logMessages.push("ERROR placing image: " + placeError.message); + } + + // 프레임 테두리 및 채우기 제거 + imageFrame.strokeWeight = 0; + imageFrame.strokeColor = "None"; + imageFrame.fillColor = "None"; + + // 투명도 100%로 설정 + try { + imageFrame.transparencySettings.blendingSettings.opacity = 100; + } catch (e) {} + } + + var imageProcessingTime = new Date() - sectionStartTime; + logMessages.push("✓ Image processing completed in " + formatElapsedTime(imageProcessingTime)); + + // 문서 마무리 + sectionStartTime = new Date(); + logMessages.push("\n=== Finalizing Document ==="); + logMessages.push("Total pages created: " + mainDoc.pages.length); + logMessages.push("Total images processed: " + imageFiles.length); + + // 깔끔한 문서 보기 설정 + if (mainDoc.pages.length > 0) { + app.activeWindow.activePage = mainDoc.pages.item(0); + + try { + app.activeWindow.viewPreferences.showFrameEdges = false; + app.activeWindow.viewPreferences.showRulers = false; + app.activeWindow.viewPreferences.showGuides = false; + app.activeWindow.viewPreferences.showBaselineGrid = false; + app.activeWindow.viewPreferences.showDocumentGrid = false; + } catch (e) { + logMessages.push("Note: Could not adjust all view preferences"); + } + } + + var finalizationTime = new Date() - sectionStartTime; + logMessages.push("✓ Document finalization completed in " + formatElapsedTime(finalizationTime)); + + // EPUB 내보내기 + sectionStartTime = new Date(); + var exportResult = exportToEPUB(root_path, mainDoc, logMessages); + + var exportTime = new Date() - sectionStartTime; + + if (exportResult.success) { + logMessages.push("✓ EPUB export complete: " + exportResult.filePath); + logMessages.push("✓ EPUB export completed in " + formatElapsedTime(exportTime)); + } else { + logMessages.push("✗ EPUB export failed: " + exportResult.error); + logMessages.push("✗ EPUB export failed after " + formatElapsedTime(exportTime)); + } + + // 스크립트 실행 요약 + var scriptEndTime = new Date(); + var totalTime = scriptEndTime - scriptStartTime; + + logMessages.push("\n=== Script Execution Summary ==="); + logMessages.push("Script end time: " + scriptEndTime.toString()); + logMessages.push("Total execution time: " + formatElapsedTime(totalTime)); + logMessages.push("Average time per image: " + formatElapsedTime(totalTime / imageFiles.length)); + logMessages.push("Images processed per minute: " + (imageFiles.length / (totalTime / 60000)).toFixed(1)); + + // 성능 분석 + logMessages.push("\n=== Performance Breakdown ==="); + logMessages.push("• Initialization: " + formatElapsedTime(initTime)); + logMessages.push("• Image discovery: " + formatElapsedTime(discoveryTime)); + logMessages.push("• Document creation: " + formatElapsedTime(docCreationTime)); + logMessages.push("• Document sizing: " + formatElapsedTime(sizingTime)); + logMessages.push("• Image processing: " + formatElapsedTime(imageProcessingTime)); + logMessages.push("• Document finalization: " + formatElapsedTime(finalizationTime)); + logMessages.push("• EPUB export: " + formatElapsedTime(exportTime)); + + writeLog(root_path, logMessages.join("\n")); + + } catch (error) { + // 오류 처리 + var scriptEndTime = new Date(); + var totalTime = scriptEndTime - scriptStartTime; + + var errorLog = "=== Script Error ===\n"; + errorLog += "Error occurred after " + formatElapsedTime(totalTime) + "\n"; + errorLog += "Error time: " + scriptEndTime.toString() + "\n"; + errorLog += "Message: " + error.message + "\n"; + if (error.line) errorLog += "Line: " + error.line + "\n"; + errorLog += "Stack: " + error.toString() + "\n"; + logMessages.push(errorLog); + writeLog(root_path, logMessages.join("\n")); + + // 문서 정리 + if (mainDoc && mainDoc.isValid) { + try { mainDoc.close(SaveOptions.NO); } catch (e) {} + } + } +} \ No newline at end of file