Spaces:
Paused
Paused
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """ | |
| 3D Flipbook Viewer (Gradio) β μ 체 μμ€ | |
| μ΅μ’ μμ : 2025-05-18 | |
| """ | |
| # ββββββββββββββββββββββββββββ | |
| # κΈ°λ³Έ λͺ¨λ | |
| # ββββββββββββββββββββββββββββ | |
| import os | |
| import shutil | |
| import uuid | |
| import json | |
| import logging | |
| import traceback | |
| from pathlib import Path | |
| # μΈλΆ λΌμ΄λΈλ¬λ¦¬ | |
| import gradio as gr | |
| from PIL import Image | |
| import fitz # PyMuPDF | |
| # ββββββββββββββββββββββββββββ | |
| # λ‘κΉ μ€μ | |
| # ββββββββββββββββββββββββββββ | |
| logging.basicConfig( | |
| level=logging.INFO, # νμνλ©΄ DEBUG | |
| format="%(asctime)s [%(levelname)s] %(message)s", | |
| filename="app.log", # λμΌ λλ ν°λ¦¬μ λ‘κ·Έ νμΌ μμ± | |
| filemode="a", | |
| ) | |
| logging.info("π Flipbook app started") | |
| # ββββββββββββββββββββββββββββ | |
| # μμ / κ²½λ‘ | |
| # ββββββββββββββββββββββββββββ | |
| 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") # μΉμΌλ‘ λ ΈμΆλλ μμΉ | |
| # λλ ν°λ¦¬ 보μ₯ | |
| for d in [TEMP_DIR, UPLOAD_DIR, OUTPUT_DIR, THUMBS_DIR, HTML_DIR]: | |
| os.makedirs(d, exist_ok=True) | |
| # ββββββββββββββββββββββββββββ | |
| # μ νΈ ν¨μ | |
| # ββββββββββββββββββββββββββββ | |
| def create_thumbnail(src: str, dst: str, size=(300, 300)) -> str | None: | |
| """μλ³Έ μ΄λ―Έμ§λ₯Ό μΈλ€μΌλ‘ μ μ₯""" | |
| try: | |
| with Image.open(src) as im: | |
| im.thumbnail(size, Image.LANCZOS) | |
| im.save(dst) | |
| return dst | |
| except Exception as e: | |
| logging.error("Thumbnail error: %s", e) | |
| return None | |
| # ββββββββββββββββββββββββββββ | |
| # PDF β μ΄λ―Έμ§ | |
| # ββββββββββββββββββββββββββββ | |
| def process_pdf(pdf_path: str, session_id: str) -> list[dict]: | |
| pages_info = [] | |
| out_dir = os.path.join(OUTPUT_DIR, session_id) | |
| th_dir = os.path.join(THUMBS_DIR, session_id) | |
| os.makedirs(out_dir, exist_ok=True) | |
| os.makedirs(th_dir, exist_ok=True) | |
| try: | |
| pdf_doc = fitz.open(pdf_path) | |
| for idx, page in enumerate(pdf_doc): | |
| pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) # 2Γ ν΄μλ | |
| img_path = os.path.join(out_dir, f"page_{idx+1}.png") | |
| pix.save(img_path) | |
| thumb_path = os.path.join(th_dir, f"thumb_{idx+1}.png") | |
| create_thumbnail(img_path, thumb_path) | |
| html_overlay = ( | |
| """ | |
| <div style="position:absolute;top:50px;left:50px; | |
| background:rgba(255,255,255,.7);padding:10px; | |
| border-radius:5px;"> | |
| <div style="font-size:18px;font-weight:bold;color:#333;"> | |
| μΈν°λν°λΈ νλ¦½λΆ μμ | |
| </div> | |
| <div style="margin-top:5px;color:#666;"> | |
| μ΄ νμ΄μ§λ μΈν°λν°λΈ 컨ν μΈ κΈ°λ₯μ 보μ¬μ€λλ€. | |
| </div> | |
| </div> | |
| """ | |
| if idx == 0 else None | |
| ) | |
| pages_info.append( | |
| { | |
| "src": f"./temp/output/{session_id}/page_{idx+1}.png", | |
| "thumb": f"./temp/output/thumbs/{session_id}/thumb_{idx+1}.png", | |
| "title": f"νμ΄μ§ {idx+1}", | |
| "htmlContent": html_overlay, | |
| } | |
| ) | |
| logging.info("PDF page %d β %s", idx + 1, img_path) | |
| return pages_info | |
| except Exception as e: | |
| logging.error("process_pdf() failed: %s", e) | |
| return [] | |
| # ββββββββββββββββββββββββββββ | |
| # μ΄λ―Έμ§ μ λ‘λ μ²λ¦¬ | |
| # ββββββββββββββββββββββββββββ | |
| def process_images(img_paths: list[str], session_id: str) -> list[dict]: | |
| pages_info = [] | |
| out_dir = os.path.join(OUTPUT_DIR, session_id) | |
| th_dir = os.path.join(THUMBS_DIR, session_id) | |
| os.makedirs(out_dir, exist_ok=True) | |
| os.makedirs(th_dir, exist_ok=True) | |
| for i, src in enumerate(img_paths): | |
| try: | |
| dst = os.path.join(out_dir, f"image_{i+1}.png") | |
| shutil.copy(src, dst) | |
| thumb = os.path.join(th_dir, f"thumb_{i+1}.png") | |
| create_thumbnail(src, thumb) | |
| if i == 0: | |
| html_overlay = """ | |
| <div style="position:absolute;top:50px;left:50px; | |
| background:rgba(255,255,255,.7);padding:10px; | |
| border-radius:5px;"> | |
| <div style="font-size:18px;font-weight:bold;color:#333;"> | |
| μ΄λ―Έμ§ κ°€λ¬λ¦¬ | |
| </div> | |
| <div style="margin-top:5px;color:#666;"> | |
| κ°€λ¬λ¦¬μ 첫 λ²μ§Έ μ΄λ―Έμ§μ λλ€. | |
| </div> | |
| </div> | |
| """ | |
| elif i == 1: | |
| html_overlay = """ | |
| <div style="position:absolute;top:50px;left:50px; | |
| background:rgba(255,255,255,.7);padding:10px; | |
| border-radius:5px;"> | |
| <div style="font-size:18px;font-weight:bold;color:#333;"> | |
| λ λ²μ§Έ μ΄λ―Έμ§ | |
| </div> | |
| <div style="margin-top:5px;color:#666;"> | |
| νμ΄μ§ λͺ¨μ리λ₯Ό λλκ·Έν΄ λ겨보μΈμ. | |
| </div> | |
| </div> | |
| """ | |
| else: | |
| html_overlay = 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_overlay, | |
| } | |
| ) | |
| logging.info("Image %d copied β %s", i + 1, dst) | |
| except Exception as e: | |
| logging.error("process_images() error (%s): %s", src, e) | |
| return pages_info | |
| # ββββββββββββββββββββββββββββ | |
| # νλ¦½λΆ HTML μμ± | |
| # ββββββββββββββββββββββββββββ | |
| def generate_flipbook_html( | |
| pages_info: list[dict], session_id: str, view_mode: str, skin: str | |
| ) -> str: | |
| # None κ°μ JSON μ§λ ¬ν μ μ μ κ±° | |
| for p in pages_info: | |
| if p.get("htmlContent") is None: | |
| p.pop("htmlContent", None) | |
| pages_json = json.dumps(pages_info, ensure_ascii=False) | |
| html_file = f"flipbook_{session_id}.html" | |
| html_path = os.path.join(HTML_DIR, html_file) | |
| html = f""" | |
| <!DOCTYPE html> | |
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>3D Flipbook</title> | |
| <link rel="stylesheet" href="/public/libs/flipbook/css/flipbook.style.css"> | |
| <script src="/public/libs/flipbook/js/flipbook.min.js"></script> | |
| <script src="/public/libs/flipbook/js/flipbook.webgl.min.js"></script> | |
| <style> | |
| html,body{{margin:0;height:100%;overflow:hidden}} | |
| #flipbook-container{{position:absolute;inset:0}} | |
| .loading{{position:absolute;top:50%;left:50%; | |
| transform:translate(-50%,-50%);text-align:center;font-family:sans-serif}} | |
| .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(0)}}100%{{transform:rotate(360deg)}}}} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="flipbook-container"></div> | |
| <div id="loading" class="loading"> | |
| <div class="spinner"></div> | |
| <div>νλ¦½λΆ λ‘λ© μ€...</div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded',()=>{ | |
| const hide=()=>{{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, | |
| thumbnailsOnStart:true, | |
| btnThumbs:{{enabled:true}}, | |
| btnPrint:{{enabled:true}}, | |
| btnDownloadPages:{{enabled:true}}, | |
| btnDownloadPdf:{{enabled:true}}, | |
| btnShare:{{enabled:true}}, | |
| btnSound:{{enabled:true}}, | |
| btnExpand:{{enabled:true}} }}; | |
| new FlipBook(document.getElementById('flipbook-container'),options); | |
| setTimeout(hide,1000); | |
| }}catch(e){{console.error(e);alert('νλ¦½λΆ μ΄κΈ°ν μ€λ₯:'+e.message);}} | |
| }); | |
| </script> | |
| </body></html> | |
| """ | |
| Path(html_path).write_text(html, encoding="utf-8") | |
| public_url = f"/public/flipbooks/{html_file}" | |
| # μ¬μ©μμκ² λλ €μ€ λ§ν¬ λ©μ΄λ¦¬ | |
| return f""" | |
| <div style="text-align:center;padding:20px;background:#f9f9f9;border-radius:5px"> | |
| <h2 style="margin:0;color:#333">ν립λΆμ΄ μ€λΉλμμ΅λλ€!</h2> | |
| <p style="margin:15px 0">λ²νΌμ λλ¬ μ μ°½μμ νμΈνμΈμ.</p> | |
| <a href="{public_url}" target="_blank" | |
| style="display:inline-block;background:#4caf50;color:#fff; | |
| padding:12px 24px;border-radius:4px;font-weight:bold;font-size:16px"> | |
| νλ¦½λΆ μ΄κΈ° | |
| </a> | |
| </div> | |
| """ | |
| # ββββββββββββββββββββββββββββ | |
| # μ½λ°±: PDF μ λ‘λ | |
| # ββββββββββββββββββββββββββββ | |
| def create_flipbook_from_pdf( | |
| pdf_file: gr.File | None, view_mode="2d", skin="light" | |
| ): | |
| session_id = str(uuid.uuid4()) | |
| debug: list[str] = [] | |
| if not pdf_file: | |
| return ( | |
| "<div style='color:red;padding:20px;'>PDF νμΌμ μ λ‘λνμΈμ.</div>", | |
| "No file", | |
| ) | |
| try: | |
| pdf_path = pdf_file.name | |
| debug.append(f"PDF path: {pdf_path}") | |
| pages_info = process_pdf(pdf_path, session_id) | |
| debug.append(f"Extracted pages: {len(pages_info)}") | |
| if not pages_info: | |
| raise RuntimeError("PDF μ²λ¦¬ κ²°κ³Όκ° λΉμ΄ μμ΅λλ€.") | |
| html_block = generate_flipbook_html( | |
| pages_info, session_id, view_mode, skin | |
| ) | |
| return html_block, "\n".join(debug) | |
| except Exception as e: | |
| tb = traceback.format_exc() | |
| logging.error(tb) | |
| debug.extend(["β ERROR βββ", tb]) | |
| return ( | |
| f"<div style='color:red;padding:20px;'>μ€λ₯: {e}</div>", | |
| "\n".join(debug), | |
| ) | |
| # ββββββββββββββββββββββββββββ | |
| # μ½λ°±: μ΄λ―Έμ§ μ λ‘λ | |
| # ββββββββββββββββββββββββββββ | |
| def create_flipbook_from_images( | |
| images: list[gr.File] | None, view_mode="2d", skin="light" | |
| ): | |
| session_id = str(uuid.uuid4()) | |
| debug: list[str] = [] | |
| if not images: | |
| return ( | |
| "<div style='color:red;padding:20px;'>μ΄λ―Έμ§λ₯Ό νλ μ΄μ μ λ‘λνμΈμ.</div>", | |
| "No images", | |
| ) | |
| try: | |
| img_paths = [f.name for f in images] | |
| debug.append(f"Images: {img_paths}") | |
| pages_info = process_images(img_paths, session_id) | |
| debug.append(f"Processed: {len(pages_info)}") | |
| if not pages_info: | |
| raise RuntimeError("μ΄λ―Έμ§ μ²λ¦¬ μ€ν¨") | |
| html_block = generate_flipbook_html( | |
| pages_info, session_id, view_mode, skin | |
| ) | |
| return html_block, "\n".join(debug) | |
| except Exception as e: | |
| tb = traceback.format_exc() | |
| logging.error(tb) | |
| debug.extend(["β ERROR βββ", tb]) | |
| return ( | |
| f"<div style='color:red;padding:20px;'>μ€λ₯: {e}</div>", | |
| "\n".join(debug), | |
| ) | |
| # ββββββββββββββββββββββββββββ | |
| # Gradio UI | |
| # ββββββββββββββββββββββββββββ | |
| with gr.Blocks(title="3D Flipbook Viewer") as demo: | |
| gr.Markdown("# 3D Flipbook Viewer\nPDF λλ μ΄λ―Έμ§λ₯Ό μ λ‘λν΄ μΈν°λν°λΈ ν립λΆμ λ§λμΈμ.") | |
| with gr.Tabs(): | |
| # PDF ν | |
| with gr.TabItem("PDF μ λ‘λ"): | |
| pdf_file = gr.File(label="PDF νμΌ", file_types=[".pdf"]) | |
| with gr.Accordion("κ³ κΈ μ€μ ", open=False): | |
| pdf_view = gr.Radio( | |
| ["webgl", "3d", "2d", "swipe"], | |
| value="2d", | |
| label="λ·° λͺ¨λ", | |
| ) | |
| pdf_skin = gr.Radio( | |
| ["light", "dark", "gradient"], | |
| value="light", | |
| label="μ€ν¨", | |
| ) | |
| pdf_btn = gr.Button("PDF β ν립λΆ", variant="primary") | |
| pdf_out = gr.HTML() | |
| pdf_dbg = gr.Textbox(label="λλ²κ·Έ", lines=10) | |
| pdf_btn.click( | |
| create_flipbook_from_pdf, | |
| inputs=[pdf_file, pdf_view, pdf_skin], | |
| outputs=[pdf_out, pdf_dbg], | |
| ) | |
| # μ΄λ―Έμ§ ν | |
| with gr.TabItem("μ΄λ―Έμ§ μ λ‘λ"): | |
| imgs = gr.File( | |
| label="μ΄λ―Έμ§ νμΌλ€", | |
| file_types=["image"], | |
| file_count="multiple", | |
| ) | |
| with gr.Accordion("κ³ κΈ μ€μ ", open=False): | |
| img_view = gr.Radio( | |
| ["webgl", "3d", "2d", "swipe"], | |
| value="2d", | |
| label="λ·° λͺ¨λ", | |
| ) | |
| img_skin = gr.Radio( | |
| ["light", "dark", "gradient"], | |
| value="light", | |
| label="μ€ν¨", | |
| ) | |
| img_btn = gr.Button("μ΄λ―Έμ§ β ν립λΆ", variant="primary") | |
| img_out = gr.HTML() | |
| img_dbg = gr.Textbox(label="λλ²κ·Έ", lines=10) | |
| img_btn.click( | |
| create_flipbook_from_images, | |
| inputs=[imgs, img_view, img_skin], | |
| outputs=[img_out, img_dbg], | |
| ) | |
| gr.Markdown( | |
| "### μ¬μ©λ²\n" | |
| "1. PDF λλ μ΄λ―Έμ§ νμ μ ννκ³ νμΌμ μ λ‘λν©λλ€.\n" | |
| "2. νμνλ©΄ λ·° λͺ¨λ/μ€ν¨μ λ°κΏλλ€.\n" | |
| "3. βν립λΆβ λ²νΌμ λλ₯΄λ©΄ κ²°κ³Όκ° μλ λΉλλ€." | |
| ) | |
| # ββββββββββββββββββββββββββββ | |
| # μ€ν | |
| # ββββββββββββββββββββββββββββ | |
| if __name__ == \"__main__\": | |
| demo.launch(debug=True) # share=True νμ μ μΆκ° | |