image cutting -> indesign script

This commit is contained in:
jeon-yeongjae 2025-07-18 18:25:05 +07:00
commit 93e2f2477b
4 changed files with 2042 additions and 0 deletions

View File

@ -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())

View File

@ -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())

770
image_cutting/main.py Normal file
View File

@ -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

467
indesign_script/image_to_epub.jsx Executable file
View File

@ -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) {}
}
}
}