# Gradio 및 Pillow 설치 (이미 실행했다면 생략 가능) # !pip install gradio pillow -q import gradio as gr from PIL import Image import os import re import io import tempfile # --- 이전 코드에서 가져온 헬퍼 함수들 (변경 없음) --- def natural_sort_key(s): name_without_extension = os.path.splitext(s)[0] return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', name_without_extension)] def resize_and_pad(img, target_size, background_color=(255, 255, 255)): original_width, original_height = img.size target_width, target_height = target_size img_copy = img.copy() try: img_copy.thumbnail((target_width, target_height), Image.Resampling.LANCZOS) except AttributeError: img_copy.thumbnail((target_width, target_height), Image.ANTIALIAS) new_img = Image.new("RGB", target_size, background_color) paste_x = (target_width - img_copy.width) // 2 paste_y = (target_height - img_copy.height) // 2 new_img.paste(img_copy, (paste_x, paste_y)) return new_img # --- Gradio 인터페이스를 위한 핵심 함수 (변경 없음) --- def images_to_pdf_gradio(uploaded_files, resize_option, output_filename="converted_images.pdf"): status_messages = [] if not uploaded_files: return None, "오류: 이미지를 업로드해주세요." file_info_list = [] for file_obj in uploaded_files: temp_path = "" original_name = "" if hasattr(file_obj, 'name') and isinstance(file_obj.name, str): temp_path = file_obj.name if hasattr(file_obj, 'orig_name') and isinstance(file_obj.orig_name, str): original_name = file_obj.orig_name else: original_name = os.path.basename(temp_path) elif isinstance(file_obj, str): temp_path = file_obj original_name = os.path.basename(temp_path) else: status_messages.append(f"⚠️ 경고: 알 수 없는 파일 객체 타입: {type(file_obj)}. 이 파일은 건너뜁니다.") continue file_info_list.append({'path': temp_path, 'orig_name': original_name}) if not file_info_list: final_status = "\n".join(status_messages) + "\n오류: 유효한 파일 정보를 가져올 수 없습니다." return None, final_status try: sorted_files_info = sorted(file_info_list, key=lambda f_info: natural_sort_key(f_info['orig_name'])) except Exception as e: final_status = "\n".join(status_messages) + f"\n파일 정렬 중 오류: {e}." return None, final_status pil_images = [] target_size = None processed_count = 0 if resize_option == "첫 번째 이미지에 맞춤" and sorted_files_info: try: first_file_info = sorted_files_info[0] with Image.open(first_file_info['path']) as img_for_size_orig: img_for_size = img_for_size_orig.copy() if img_for_size.mode == 'RGBA' or img_for_size.mode == 'P': img_for_size = img_for_size.convert('RGB') target_size = img_for_size.size status_messages.append(f"기준 이미지 크기: {target_size} (첫 번째 이미지 '{first_file_info['orig_name']}' 기준)") except Exception as e: status_messages.append(f"⚠️ 경고: 첫 번째 이미지 크기 설정 중 오류 - {e}. 원본 크기로 진행합니다.") resize_option = "원본 크기 유지" for file_info in sorted_files_info: original_filename = file_info['orig_name'] temp_filepath = file_info['path'] try: with Image.open(temp_filepath) as img_orig: img = img_orig.copy() if img.mode == 'RGBA' or img.mode == 'P': img = img.convert('RGB') if resize_option == "첫 번째 이미지에 맞춤" and target_size: img = resize_and_pad(img, target_size) pil_images.append(img) status_messages.append(f"✅ 처리 완료: {original_filename}") processed_count += 1 except Exception as e: status_messages.append(f"❌ 오류: '{original_filename}' 처리 중 문제 발생 - {e}") if not pil_images: final_status = "\n".join(status_messages) + "\n\n오류: PDF로 변환할 유효한 이미지가 없습니다." return None, final_status pdf_buffer = io.BytesIO() try: pil_images[0].save( pdf_buffer, format="PDF", save_all=True, append_images=pil_images[1:], resolution=100.0 ) pdf_buffer.seek(0) file_prefix = os.path.splitext(output_filename)[0] with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf", prefix=file_prefix + "_") as tmp_pdf: tmp_pdf.write(pdf_buffer.getvalue()) tmp_pdf_path = tmp_pdf.name final_status = "\n".join(status_messages) + f"\n\n🎉 성공: 총 {processed_count}개의 이미지로 PDF 생성 완료! 아래 버튼으로 다운로드하세요." return tmp_pdf_path, final_status except Exception as e: final_status = "\n".join(status_messages) + f"\n\n❌ 오류: PDF 파일 저장 중 문제 발생 - {e}" return None, final_status # --- Gradio 인터페이스 정의 (DownloadButton 초기 상태 변경) --- with gr.Blocks(theme=gr.themes.Default()) as demo: gr.Markdown( """ # 🖼️ 이미지(PNG, JPG)를 PDF로 변환 📄 여러 이미지 파일을 업로드하고, 필요에 따라 크기 조절 옵션을 선택한 후 'PDF 생성' 버튼을 누르세요. 파일은 이름순 (예: 1.jpg, 2.png, 10.jpg)으로 정렬됩니다. """ ) with gr.Row(): with gr.Column(scale=2): image_files_input = gr.File( label="1. 이미지 파일 업로드 (다중 선택 가능)", file_count="multiple", file_types=["image"], ) resize_option_input = gr.Radio( label="2. 이미지 크기 조절 옵션", choices=["첫 번째 이미지에 맞춤", "원본 크기 유지"], value="첫 번째 이미지에 맞춤", ) output_pdf_name_input = gr.Textbox( label="3. 생성될 PDF 파일 이름 (확장자 포함)", value="converted_images.pdf", placeholder="예: my_album.pdf" ) submit_button = gr.Button("🚀 PDF 생성 실행", variant="primary", elem_id="generate-button") pdf_download_button = gr.DownloadButton( "⬇️ 병합된 PDF 다운로드 (생성 후 활성화)", # 초기 레이블 변경 value=None, variant="secondary", visible=True, # 처음부터 보이도록 변경 interactive=False # 처음에는 비활성화 # elem_id="download-button-custom-style" ) with gr.Column(scale=3): status_message_output = gr.Textbox( label="📝 처리 상태 및 결과 메시지", lines=10, interactive=False, show_copy_button=True ) def process_and_update_ui(uploaded_files, resize_option, output_filename): actual_pdf_file_path, status_msg = images_to_pdf_gradio(uploaded_files, resize_option, output_filename) download_button_update_dict = {} if actual_pdf_file_path: download_button_update_dict = { "value": actual_pdf_file_path, "label": "⬇️ 병합된 PDF 다운로드", "interactive": True # 활성화 } else: download_button_update_dict = { "value": None, "label": "⬇️ PDF 다운로드 (생성 실패 또는 파일 없음)", "interactive": False # 비활성화 유지 } return status_msg, gr.update(**download_button_update_dict) submit_button.click( fn=process_and_update_ui, inputs=[image_files_input, resize_option_input, output_pdf_name_input], outputs=[status_message_output, pdf_download_button] ) # CSS 주석은 이전과 동일하게 유지 (선택 사항) # demo.css = """ ... """ gr.Markdown( """ --- **💡 사용 팁:** - 파일 이름에 숫자가 포함된 경우 자연 정렬됩니다 (예: `image1.jpg`, `image2.jpg`, `image10.jpg`). - '첫 번째 이미지에 맞춤' 옵션 선택 시, 모든 이미지가 첫 번째 이미지의 가로/세로 크기에 맞춰집니다. 비율은 유지되며, 남는 공간은 흰색으로 채워집니다. - 대용량 파일이나 많은 수의 파일을 한 번에 처리하면 시간이 오래 걸릴 수 있습니다. """ ) demo.launch(debug=True)