Spaces:
Paused
Paused
| 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 = """ | |
| <div style="position: absolute; top: 50px; left: 50px; background-color: rgba(255,255,255,0.7); padding: 10px; border-radius: 5px;"> | |
| <div style="color: #333; font-size: 18px; font-weight: bold;">μΈν°λν°λΈ νλ¦½λΆ μμ </div> | |
| <div style="color: #666; margin-top: 5px;">μ΄ νμ΄μ§λ μΈν°λν°λΈ 컨ν μΈ κΈ°λ₯μ 보μ¬μ€λλ€.</div> | |
| </div> | |
| """ 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 = """ | |
| <div style=\"position: absolute; top: 50px; left: 50px; background-color: rgba(255,255,255,0.7); padding: 10px; border-radius: 5px;\"> | |
| <div style=\"color: #333; font-size: 18px; font-weight: bold;\">μ΄λ―Έμ§ κ°€λ¬λ¦¬</div> | |
| <div style=\"color: #666; margin-top: 5px;\">κ°€λ¬λ¦¬μ 첫 λ²μ§Έ μ΄λ―Έμ§μ λλ€.</div> | |
| </div> | |
| """ | |
| elif i == 1: | |
| html_content = """ | |
| <div style=\"position: absolute; top: 50px; left: 50px; background-color: rgba(255,255,255,0.7); padding: 10px; border-radius: 5px;\"> | |
| <div style=\"color: #333; font-size: 18px; font-weight: bold;\">λ λ²μ§Έ μ΄λ―Έμ§</div> | |
| <div style=\"color: #666; margin-top: 5px;\">νμ΄μ§λ₯Ό λκΈ°κ±°λ λͺ¨μ리λ₯Ό λλκ·Ένμ¬ μ΄λ―Έμ§λ₯Ό νμν μ μμ΅λλ€.</div> | |
| </div> | |
| """ | |
| 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 ( | |
| "<div style='color:red;padding:20px;'>PDF νμΌμ μ λ‘λν΄μ£ΌμΈμ.</div>", | |
| "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"<div style='color:red;padding:20px;'>μ€λ₯κ° λ°μνμ΅λλ€: {e}</div>", | |
| "\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 ( | |
| "<div style='color:red;padding:20px;'>μ΅μ ν κ° μ΄μμ μ΄λ―Έμ§λ₯Ό μ λ‘λν΄μ£ΌμΈμ.</div>", | |
| "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"<div style='color:red;padding:20px;'>μ€λ₯κ° λ°μνμ΅λλ€: {e}</div>", | |
| "\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""" | |
| <!DOCTYPE html> | |
| <html lang=\"ko\"> | |
| <head> | |
| <meta charset=\"UTF-8\"> | |
| <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> | |
| <title>3D ν립λΆ</title> | |
| <link rel=\"stylesheet\" type=\"text/css\" href=\"../flipbook.css\"> | |
| <style> | |
| body, html {{ margin: 0; padding: 0; height: 100%; overflow: hidden; }} | |
| #flipbook-container {{ width: 100%; height: 100%; position: absolute; top: 0; left: 0; }} | |
| .loading {{ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; font-family: Arial, sans-serif; }} | |
| .loading .spinner {{ width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 20px; }} | |
| @keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }} | |
| </style> | |
| <script src=\"../flipbook.js\"></script> | |
| <script src=\"../flipbook.webgl.js\"></script> | |
| <script src=\"../flipbook.swipe.js\"></script> | |
| <script src=\"../flipbook.scroll.js\"></script> | |
| <script src=\"../flipbook.book3.js\"></script> | |
| </head> | |
| <body> | |
| <div id=\"flipbook-container\"></div> | |
| <div id=\"loading\" class=\"loading\"> | |
| <div class=\"spinner\"></div> | |
| <div>νλ¦½λΆ λ‘λ© μ€...</div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function () {{ | |
| function hideLoading() {{ document.getElementById('loading').style.display = 'none'; }} | |
| try {{ | |
| const options = {{ | |
| pages: {pages_json}, | |
| viewMode: '{view_mode}', | |
| skin: '{skin}', | |
| responsiveView: true, | |
| singlePageMode: false, | |
| singlePageModeIfMobile: true, | |
| pageFlipDuration: 1, | |
| sound: true, | |
| backgroundMusic: false, | |
| thumbnailsOnStart: true, | |
| btnThumbs: {{ enabled: true }}, | |
| btnPrint: {{ enabled: true }}, | |
| btnDownloadPages: {{ enabled: true }}, | |
| btnDownloadPdf: {{ enabled: true }}, | |
| btnShare: {{ enabled: true }}, | |
| btnSound: {{ enabled: true }}, | |
| btnExpand: {{ enabled: true }}, | |
| rightToLeft: false, | |
| autoplayOnStart: false, | |
| autoplayInterval: 3000, | |
| }}; | |
| const container = document.getElementById('flipbook-container'); | |
| if (container) {{ | |
| new FlipBook(container, options); | |
| setTimeout(hideLoading, 1000); | |
| }} else {{ | |
| console.error('Flipbook container not found'); | |
| alert('μ€λ₯: νλ¦½λΆ μ»¨ν μ΄λλ₯Ό μ°Ύμ μ μμ΅λλ€.'); | |
| }} | |
| }} catch (error) {{ | |
| console.error('Error initializing flipbook:', error); | |
| alert('νλ¦½λΆ μ΄κΈ°ν μ€ μ€λ₯κ° λ°μνμ΅λλ€: ' + error.message); | |
| document.getElementById('loading').innerHTML = '<div>μ€λ₯κ° λ°μνμ΅λλ€.</div>'; | |
| }} | |
| }}); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| with open(html_path, "w", encoding="utf-8") as f: | |
| f.write(html_content) | |
| public_url = f"/public/flipbooks/{html_filename}" | |
| link_html = f""" | |
| <div style=\"text-align:center; padding:20px; background-color:#f9f9f9; border-radius:5px; margin-bottom:20px;\"> | |
| <h2 style=\"margin-top:0; color:#333;\">ν립λΆμ΄ μ€λΉλμμ΅λλ€!</h2> | |
| <p style=\"margin-bottom:20px;\">μλ λ²νΌμ ν΄λ¦νμ¬ ν립λΆμ μ μ°½μμ μ΄μ΄λ³΄μΈμ.</p> | |
| <a href=\"{public_url}\" target=\"_blank\" style=\"display:inline-block; background-color:#4CAF50; color:white; padding | |
| <div style="text-align:center; padding:20px; background-color:#f9f9f9; border-radius:5px; margin-bottom:20px;"> | |
| <h2 style="margin-top:0; color:#333;">ν립λΆμ΄ μ€λΉλμμ΅λλ€!</h2> | |
| <p style="margin-bottom:20px;">μλ λ²νΌμ ν΄λ¦νμ¬ ν립λΆμ μ μ°½μμ μ΄μ΄λ³΄μΈμ.</p> | |
| <a href="{public_url}" target="_blank" style="display:inline-block; background-color:#4CAF50; color:white; padding:12px 24px; text-decoration:none; border-radius:4px; font-weight:bold; font-size:16px;">νλ¦½λΆ μ΄κΈ°</a> | |
| </div> | |
| <div style="margin-top:20px; padding:15px; background-color:#f5f5f5; border-radius:5px; line-height:1.5;"> | |
| <h3 style="margin-top:0; color:#333;">μ¬μ© ν:</h3> | |
| <ul style="margin:10px 0; padding-left:20px;"> | |
| <li>νμ΄μ§ λͺ¨μ리λ₯Ό λλκ·Ένμ¬ λκΈΈ μ μμ΅λλ€.</li> | |
| <li>νλ¨ ν΄λ°μ μμ΄μ½μ μ¬μ©νμ¬ λ€μν κΈ°λ₯μ νμ©νμΈμ.</li> | |
| <li>μ 체νλ©΄ λ²νΌμ ν΄λ¦νμ¬ λ ν° νλ©΄μΌλ‘ λ³Ό μ μμ΅λλ€.</li> | |
| </ul> | |
| <div style="margin-top:10px; padding:10px; background-color:#e8f4fd; border-left:4px solid #2196F3; border-radius:2px;"> | |
| <strong>μ°Έκ³ :</strong> ν립λΆμ 2D λͺ¨λμμ κ°μ₯ μμ μ μΌλ‘ μλν©λλ€. | |
| </div> | |
| </div> | |
| <div style="margin-top:15px; background-color:#f5f5f5; border-radius:5px; padding:10px;"> | |
| <details> | |
| <summary style="cursor:pointer; color:#2196F3; font-weight:bold;">κΈ°μ μ μΈλΆμ¬ν (κ°λ°μμ©)</summary> | |
| <div style="margin-top:10px;"> | |
| <p>μΈμ ID: {session_id}</p> | |
| <p>HTML νμΌ κ²½λ‘: {html_path}</p> | |
| <p>νμ΄μ§ μ: {len(pages_info)}</p> | |
| <p>λ·° λͺ¨λ: {view_mode}</p> | |
| <p>μ€ν¨: {skin}</p> | |
| </div> | |
| </details> | |
| </div> | |
| """ | |
| 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 |