import os import gradio as gr import shutil import uuid from pathlib import Path import json import logging import traceback from PIL import Image import fitz # PyMuPDF for PDF handling # ──────────────────────────────── # Logging 설정 # ──────────────────────────────── logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", filename="app.log", # 실행 디렉터리에 app.log 파일 저장 filemode="a", ) logging.info("🚀 Flipbook app started") # Constants TEMP_DIR = "temp" UPLOAD_DIR = os.path.join(TEMP_DIR, "uploads") OUTPUT_DIR = os.path.join(TEMP_DIR, "output") THUMBS_DIR = os.path.join(OUTPUT_DIR, "thumbs") HTML_DIR = os.path.join("public", "flipbooks") # Directory accessible via web # Ensure directories exist for dir_path in [TEMP_DIR, UPLOAD_DIR, OUTPUT_DIR, THUMBS_DIR, HTML_DIR]: os.makedirs(dir_path, exist_ok=True) def create_thumbnail(image_path, output_path, size=(300, 300)): """Create a thumbnail from an image.""" try: with Image.open(image_path) as img: img.thumbnail(size, Image.LANCZOS) img.save(output_path) return output_path except Exception as e: logging.error("Error creating thumbnail: %s", e) return None def process_pdf(pdf_path, session_id): """Extract pages from a PDF and save as images with thumbnails.""" pages_info = [] output_folder = os.path.join(OUTPUT_DIR, session_id) thumbs_folder = os.path.join(THUMBS_DIR, session_id) os.makedirs(output_folder, exist_ok=True) os.makedirs(thumbs_folder, exist_ok=True) try: pdf_document = fitz.open(pdf_path) for page_num, page in enumerate(pdf_document): pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) image_path = os.path.join(output_folder, f"page_{page_num + 1}.png") pix.save(image_path) thumb_path = os.path.join(thumbs_folder, f"thumb_{page_num + 1}.png") create_thumbnail(image_path, thumb_path) html_content = """
인터랙티브 플립북 예제
이 페이지는 인터랙티브 컨텐츠 기능을 보여줍니다.
""" if page_num == 0 else None pages_info.append({ "src": f"./temp/output/{session_id}/page_{page_num + 1}.png", "thumb": f"./temp/output/thumbs/{session_id}/thumb_{page_num + 1}.png", "title": f"페이지 {page_num + 1}", "htmlContent": html_content, }) logging.info("Processed PDF page %d: %s", page_num + 1, image_path) return pages_info except Exception as e: logging.error("Error processing PDF: %s", e) return [] def process_images(image_paths, session_id): """Process uploaded images and create thumbnails.""" pages_info = [] output_folder = os.path.join(OUTPUT_DIR, session_id) thumbs_folder = os.path.join(THUMBS_DIR, session_id) os.makedirs(output_folder, exist_ok=True) os.makedirs(thumbs_folder, exist_ok=True) for i, img_path in enumerate(image_paths): try: dest_path = os.path.join(output_folder, f"image_{i + 1}.png") shutil.copy(img_path, dest_path) thumb_path = os.path.join(thumbs_folder, f"thumb_{i + 1}.png") create_thumbnail(img_path, thumb_path) if i == 0: html_content = """
이미지 갤러리
갤러리의 첫 번째 이미지입니다.
""" elif i == 1: html_content = """
두 번째 이미지
페이지를 넘기거나 모서리를 드래그하여 이미지를 탐색할 수 있습니다.
""" else: html_content = None pages_info.append({ "src": f"./temp/output/{session_id}/image_{i + 1}.png", "thumb": f"./temp/output/thumbs/{session_id}/thumb_{i + 1}.png", "title": f"이미지 {i + 1}", "htmlContent": html_content, }) logging.info("Processed image %d: %s", i + 1, dest_path) except Exception as e: logging.error("Error processing image %s: %s", img_path, e) return pages_info def create_flipbook_from_pdf(pdf_file, view_mode="2d", skin="light"): """Create a flipbook from an uploaded PDF.""" session_id = str(uuid.uuid4()) debug_info = [] if not pdf_file: return ( "
PDF 파일을 업로드해주세요.
", "No file uploaded", ) try: pdf_path = pdf_file.name debug_info.append(f"PDF path: {pdf_path}") # 1) PDF → 이미지 pages_info = process_pdf(pdf_path, session_id) debug_info.append(f"Number of pages: {len(pages_info)}") if not pages_info: raise RuntimeError("PDF 처리 결과가 비어 있습니다.") # 2) HTML 생성 iframe_html = generate_flipbook_html(pages_info, session_id, view_mode, skin) return iframe_html, "\n".join(debug_info) except Exception as e: tb = traceback.format_exc() logging.error(tb) debug_info.append("❌ ERROR ↓↓↓") debug_info.append(tb) return ( f"
오류가 발생했습니다: {e}
", "\n".join(debug_info), ) def create_flipbook_from_images(images, view_mode="2d", skin="light"): """Create a flipbook from uploaded images.""" session_id = str(uuid.uuid4()) debug_info = [] if not images: return ( "
최소 한 개 이상의 이미지를 업로드해주세요.
", "No images uploaded", ) try: image_paths = [img.name for img in images] debug_info.append(f"Image paths: {image_paths}") pages_info = process_images(image_paths, session_id) debug_info.append(f"Number of images processed: {len(pages_info)}") if not pages_info: raise RuntimeError("이미지 처리 결과가 비어 있습니다.") iframe_html = generate_flipbook_html(pages_info, session_id, view_mode, skin) return iframe_html, "\n".join(debug_info) except Exception as e: tb = traceback.format_exc() logging.error(tb) debug_info.append("❌ ERROR ↓↓↓") debug_info.append(tb) return ( f"
오류가 발생했습니다: {e}
", "\n".join(debug_info), ) def generate_flipbook_html(pages_info, session_id, view_mode, skin): """Generate a standalone HTML file for the flipbook and return link HTML.""" for page in pages_info: if page.get("htmlContent") is None: page.pop("htmlContent", None) if page.get("items") is None: page.pop("items", None) pages_json = json.dumps(pages_info) html_filename = f"flipbook_{session_id}.html" html_path = os.path.join(HTML_DIR, html_filename) html_content = f""" 3D 플립북
플립북 로딩 중...
""" with open(html_path, "w", encoding="utf-8") as f: f.write(html_content) public_url = f"/public/flipbooks/{html_filename}" link_html = f"""

플립북이 준비되었습니다!

아래 버튼을 클릭하여 플립북을 새 창에서 열어보세요.

플립북이 준비되었습니다!

아래 버튼을 클릭하여 플립북을 새 창에서 열어보세요.

플립북 열기

사용 팁:

참고: 플립북은 2D 모드에서 가장 안정적으로 작동합니다.
기술적 세부사항 (개발자용)

세션 ID: {session_id}

HTML 파일 경로: {html_path}

페이지 수: {len(pages_info)}

뷰 모드: {view_mode}

스킨: {skin}

""" return link_html # Define the Gradio interface with gr.Blocks(title="3D Flipbook Viewer") as demo: gr.Markdown("# 3D Flipbook Viewer") gr.Markdown(""" ## 3D 플립북 뷰어 PDF 파일이나 여러 이미지를 업로드하여 인터랙티브 3D 플립북을 만들 수 있습니다. ### 특징: - 페이지 넘김 효과와 함께 인터랙티브한 기능 제공 - 첫 페이지에는 예시로 인터랙티브 요소가 포함됨 - 툴바를 사용하거나 페이지 모서리를 드래그하여 탐색 - 썸네일 보기로 빠른 탐색 가능 - 전체 화면으로 전환하여 더 나은 보기 경험 """) with gr.Tabs(): with gr.TabItem("PDF 업로드"): pdf_file = gr.File(label="PDF 파일 업로드", file_types=[".pdf"]) with gr.Accordion("고급 설정", open=False): pdf_view_mode = gr.Radio( choices=["webgl", "3d", "2d", "swipe"], value="2d", # Changed default to 2d for better compatibility label="뷰 모드", info="WebGL: 최고 품질, 2D: 가장 안정적, 3D: 중간, Swipe: 모바일용" ) pdf_skin = gr.Radio( choices=["light", "dark", "gradient"], value="light", label="스킨", info="light: 밝은 테마, dark: 어두운 테마, gradient: 그라데이션 테마" ) pdf_create_btn = gr.Button("PDF에서 플립북 만들기", variant="primary", size="lg") pdf_debug = gr.Textbox(label="디버그 정보", visible=False) pdf_output = gr.HTML(label="플립북 결과물") # Set up PDF event handler pdf_create_btn.click( fn=create_flipbook_from_pdf, inputs=[pdf_file, pdf_view_mode, pdf_skin], outputs=[pdf_output, pdf_debug] ) with gr.TabItem("이미지 업로드"): images = gr.File(label="이미지 파일 업로드", file_types=["image"], file_count="multiple") with gr.Accordion("고급 설정", open=False): img_view_mode = gr.Radio( choices=["webgl", "3d", "2d", "swipe"], value="2d", # Changed default to 2d for better compatibility label="뷰 모드", info="WebGL: 최고 품질, 2D: 가장 안정적, 3D: 중간, Swipe: 모바일용" ) img_skin = gr.Radio( choices=["light", "dark", "gradient"], value="light", label="스킨", info="light: 밝은 테마, dark: 어두운 테마, gradient: 그라데이션 테마" ) img_create_btn = gr.Button("이미지에서 플립북 만들기", variant="primary", size="lg") img_debug = gr.Textbox(label="디버그 정보", visible=False) img_output = gr.HTML(label="플립북 결과물") # Set up image event handler img_create_btn.click( fn=create_flipbook_from_images, inputs=[images, img_view_mode, img_skin], outputs=[img_output, img_debug] ) gr.Markdown(""" ### 사용법: 1. 컨텐츠 유형에 따라 탭을 선택하세요 (PDF 또는 이미지) 2. 파일을 업로드하세요 3. 필요에 따라 고급 설정에서 뷰 모드와 스킨을 조정하세요 4. 플립북 만들기 버튼을 클릭하세요 5. 출력 영역에서 플립북과 상호작용하세요 ### 참고: - 처음 페이지에는 예시로 인터랙티브 요소와 링크가 포함되어 있습니다 - 최상의 결과를 위해 선명한 텍스트와 이미지가 있는 PDF를 사용하세요 - 지원되는 이미지 형식: JPG, PNG, GIF 등 - 플립북이 보이지 않는 경우, 2D 모드를 선택하고 다시 시도해보세요 """) # Launch the app if __name__ == "__main__": demo.launch() # Remove share=True as it's not supported in Spaces