image cutting -> indesign script
This commit is contained in:
commit
93e2f2477b
370
image_cutting/backup/main_완성.py
Normal file
370
image_cutting/backup/main_완성.py
Normal 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())
|
||||
435
image_cutting/backup/main_완성_주석없이.py
Normal file
435
image_cutting/backup/main_완성_주석없이.py
Normal 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
770
image_cutting/main.py
Normal 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
467
indesign_script/image_to_epub.jsx
Executable 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) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user