#!/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 = ( """
인터랙티브 플립북 예제
이 페이지는 인터랙티브 컨텐츠 기능을 보여줍니다.
""" 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 = """
이미지 갤러리
갤러리의 첫 번째 이미지입니다.
""" elif i == 1: html_overlay = """
두 번째 이미지
페이지 모서리를 드래그해 넘겨보세요.
""" 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""" 3D Flipbook
플립북 로딩 중...
""" Path(html_path).write_text(html, encoding="utf-8") public_url = f"/public/flipbooks/{html_file}" # 사용자에게 돌려줄 링크 덩어리 return f"""

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

버튼을 눌러 새 창에서 확인하세요.

플립북 열기
""" # ──────────────────────────── # 콜백: 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 ( "
PDF 파일을 업로드하세요.
", "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"
오류: {e}
", "\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 ( "
이미지를 하나 이상 업로드하세요.
", "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"
오류: {e}
", "\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 필요 시 추가