#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 3D Flipbook Viewer (Gradio) – 전체 소스 (SyntaxError 수정 버전) 최종 수정: 2025-05-18 """ # ──────────────────────────── # 기본 모듈 # ──────────────────────────── import os import shutil import uuid import json import logging import traceback from pathlib import Path from typing import Optional, List, Dict # 외부 라이브러리 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)) -> Optional[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]: """PDF 파일을 페이지 별 PNG로 변환하고 페이지 정보를 리스트로 리턴""" 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): # 해상도 향상을 위해 매트릭스 사용 (1.5배 정도) mat = fitz.Matrix(1.5, 1.5) pix = page.get_pixmap(matrix=mat) 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 제공 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(dst, 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: """페이지 정보를 3D Flipbook에 적용할 HTML 파일로 만들고 링크 반환""" # htmlContent가 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) # f-string 내부에서 JS용 { }를 표현하려면 {{ }} 로 써야 함 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: Optional[gr.File], view_mode: str = "2d", skin: str = "light" ): session_id = str(uuid.uuid4()) debug: List[str] = [] if not pdf_file: return ( "
PDF 파일을 업로드하세요.
", "No file", ) try: # Gradio가 넘겨준 임시 PDF 경로 uploaded_temp_path = pdf_file.name # 서버 내 임시 업로드 폴더에 안전하게 복사 filename_only = os.path.basename(uploaded_temp_path) pdf_path = os.path.join(UPLOAD_DIR, filename_only) shutil.copyfile(uploaded_temp_path, pdf_path) debug.append(f"Copied PDF to: {pdf_path}") # PDF → 페이지 이미지 변환 pages_info = process_pdf(pdf_path, session_id) debug.append(f"Extracted pages: {len(pages_info)}") if not pages_info: raise RuntimeError("PDF 처리 결과가 비어 있습니다.") # 플립북 HTML 생성 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: Optional[List[gr.File]], view_mode: str = "2d", skin: str = "light" ): session_id = str(uuid.uuid4()) debug: List[str] = [] if not images: return ( "
이미지를 하나 이상 업로드하세요.
", "No images", ) try: # Gradio가 넘겨준 임시 이미지 경로들 img_paths = [] for fobj in images: # 안전하게 temp 폴더에 복사 uploaded_temp_path = fobj.name filename_only = os.path.basename(uploaded_temp_path) local_img_path = os.path.join(UPLOAD_DIR, filename_only) shutil.copyfile(uploaded_temp_path, local_img_path) img_paths.append(local_img_path) 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 생성 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__": # 필요한 경우 share=True 등 인자로 추가 가능 demo.launch(debug=True)