ginipick commited on
Commit
08e6637
Β·
verified Β·
1 Parent(s): 898cc4c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +304 -314
app.py CHANGED
@@ -1,427 +1,417 @@
 
 
 
 
 
 
 
 
 
 
1
  import os
2
- import gradio as gr
3
  import shutil
4
  import uuid
5
- from pathlib import Path
6
  import json
7
  import logging
8
  import traceback
 
 
 
 
9
  from PIL import Image
10
- import fitz # PyMuPDF for PDF handling
11
 
12
- # ────────────────────────────────
13
- # Logging μ„€μ •
14
- # ────────────────────────────────
15
  logging.basicConfig(
16
- level=logging.INFO,
17
  format="%(asctime)s [%(levelname)s] %(message)s",
18
- filename="app.log", # μ‹€ν–‰ 디렉터리에 app.log 파일 μ €μž₯
19
  filemode="a",
20
  )
21
  logging.info("πŸš€ Flipbook app started")
22
 
23
- # Constants
24
- TEMP_DIR = "temp"
 
 
25
  UPLOAD_DIR = os.path.join(TEMP_DIR, "uploads")
26
  OUTPUT_DIR = os.path.join(TEMP_DIR, "output")
27
  THUMBS_DIR = os.path.join(OUTPUT_DIR, "thumbs")
28
- HTML_DIR = os.path.join("public", "flipbooks") # Directory accessible via web
29
-
30
- # Ensure directories exist
31
- for dir_path in [TEMP_DIR, UPLOAD_DIR, OUTPUT_DIR, THUMBS_DIR, HTML_DIR]:
32
- os.makedirs(dir_path, exist_ok=True)
33
 
 
 
 
34
 
35
- def create_thumbnail(image_path, output_path, size=(300, 300)):
36
- """Create a thumbnail from an image."""
 
 
 
37
  try:
38
- with Image.open(image_path) as img:
39
- img.thumbnail(size, Image.LANCZOS)
40
- img.save(output_path)
41
- return output_path
42
  except Exception as e:
43
- logging.error("Error creating thumbnail: %s", e)
44
  return None
45
 
46
 
47
- def process_pdf(pdf_path, session_id):
48
- """Extract pages from a PDF and save as images with thumbnails."""
 
 
49
  pages_info = []
50
- output_folder = os.path.join(OUTPUT_DIR, session_id)
51
- thumbs_folder = os.path.join(THUMBS_DIR, session_id)
52
-
53
- os.makedirs(output_folder, exist_ok=True)
54
- os.makedirs(thumbs_folder, exist_ok=True)
55
 
56
  try:
57
- pdf_document = fitz.open(pdf_path)
58
-
59
- for page_num, page in enumerate(pdf_document):
60
- pix = page.get_pixmap(matrix=fitz.Matrix(2, 2))
61
- image_path = os.path.join(output_folder, f"page_{page_num + 1}.png")
62
- pix.save(image_path)
63
 
64
- thumb_path = os.path.join(thumbs_folder, f"thumb_{page_num + 1}.png")
65
- create_thumbnail(image_path, thumb_path)
66
 
67
- html_content = """
68
- <div style="position: absolute; top: 50px; left: 50px; background-color: rgba(255,255,255,0.7); padding: 10px; border-radius: 5px;">
69
- <div style="color: #333; font-size: 18px; font-weight: bold;">μΈν„°λž™ν‹°λΈŒ ν”Œλ¦½λΆ 예제</div>
70
- <div style="color: #666; margin-top: 5px;">이 νŽ˜μ΄μ§€λŠ” μΈν„°λž™ν‹°λΈŒ 컨텐츠 κΈ°λŠ₯을 λ³΄μ—¬μ€λ‹ˆλ‹€.</div>
 
 
 
 
 
 
 
71
  </div>
72
- """ if page_num == 0 else None
73
-
74
- pages_info.append({
75
- "src": f"./temp/output/{session_id}/page_{page_num + 1}.png",
76
- "thumb": f"./temp/output/thumbs/{session_id}/thumb_{page_num + 1}.png",
77
- "title": f"νŽ˜μ΄μ§€ {page_num + 1}",
78
- "htmlContent": html_content,
79
- })
80
- logging.info("Processed PDF page %d: %s", page_num + 1, image_path)
81
 
 
 
 
 
 
 
 
 
 
82
  return pages_info
 
83
  except Exception as e:
84
- logging.error("Error processing PDF: %s", e)
85
  return []
86
 
87
 
88
- def process_images(image_paths, session_id):
89
- """Process uploaded images and create thumbnails."""
 
 
90
  pages_info = []
91
- output_folder = os.path.join(OUTPUT_DIR, session_id)
92
- thumbs_folder = os.path.join(THUMBS_DIR, session_id)
93
-
94
- os.makedirs(output_folder, exist_ok=True)
95
- os.makedirs(thumbs_folder, exist_ok=True)
96
 
97
- for i, img_path in enumerate(image_paths):
98
  try:
99
- dest_path = os.path.join(output_folder, f"image_{i + 1}.png")
100
- shutil.copy(img_path, dest_path)
101
 
102
- thumb_path = os.path.join(thumbs_folder, f"thumb_{i + 1}.png")
103
- create_thumbnail(img_path, thumb_path)
104
 
105
  if i == 0:
106
- html_content = """
107
- <div style=\"position: absolute; top: 50px; left: 50px; background-color: rgba(255,255,255,0.7); padding: 10px; border-radius: 5px;\">
108
- <div style=\"color: #333; font-size: 18px; font-weight: bold;\">이미지 가러리</div>
109
- <div style=\"color: #666; margin-top: 5px;\">가러리의 첫 번째 μ΄λ―Έμ§€μž…λ‹ˆλ‹€.</div>
 
 
 
 
 
110
  </div>
 
111
  """
112
  elif i == 1:
113
- html_content = """
114
- <div style=\"position: absolute; top: 50px; left: 50px; background-color: rgba(255,255,255,0.7); padding: 10px; border-radius: 5px;\">
115
- <div style=\"color: #333; font-size: 18px; font-weight: bold;\">두 번째 이미지</div>
116
- <div style=\"color: #666; margin-top: 5px;\">νŽ˜μ΄μ§€λ₯Ό λ„˜κΈ°κ±°λ‚˜ λͺ¨μ„œλ¦¬λ₯Ό λ“œλž˜κ·Έν•˜μ—¬ 이미지λ₯Ό 탐색할 수 μžˆμŠ΅λ‹ˆλ‹€.</div>
 
 
 
 
 
117
  </div>
 
118
  """
119
  else:
120
- html_content = None
121
-
122
- pages_info.append({
123
- "src": f"./temp/output/{session_id}/image_{i + 1}.png",
124
- "thumb": f"./temp/output/thumbs/{session_id}/thumb_{i + 1}.png",
125
- "title": f"이미지 {i + 1}",
126
- "htmlContent": html_content,
127
- })
128
- logging.info("Processed image %d: %s", i + 1, dest_path)
 
 
129
 
130
  except Exception as e:
131
- logging.error("Error processing image %s: %s", img_path, e)
132
 
133
  return pages_info
134
 
135
 
136
- def create_flipbook_from_pdf(pdf_file, view_mode="2d", skin="light"):
137
- """Create a flipbook from an uploaded PDF."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  session_id = str(uuid.uuid4())
139
- debug_info = []
140
 
141
  if not pdf_file:
142
  return (
143
- "<div style='color:red;padding:20px;'>PDF νŒŒμΌμ„ μ—…λ‘œλ“œν•΄μ£Όμ„Έμš”.</div>",
144
- "No file uploaded",
145
  )
146
 
147
  try:
148
  pdf_path = pdf_file.name
149
- debug_info.append(f"PDF path: {pdf_path}")
150
 
151
- # 1) PDF β†’ 이미지
152
  pages_info = process_pdf(pdf_path, session_id)
153
- debug_info.append(f"Number of pages: {len(pages_info)}")
154
 
155
  if not pages_info:
156
  raise RuntimeError("PDF 처리 κ²°κ³Όκ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
157
 
158
- # 2) HTML 생성
159
- iframe_html = generate_flipbook_html(pages_info, session_id, view_mode, skin)
160
- return iframe_html, "\n".join(debug_info)
 
161
 
162
  except Exception as e:
163
  tb = traceback.format_exc()
164
  logging.error(tb)
165
- debug_info.append("❌ ERROR ↓↓↓")
166
- debug_info.append(tb)
167
  return (
168
- f"<div style='color:red;padding:20px;'>였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {e}</div>",
169
- "\n".join(debug_info),
170
  )
171
 
172
 
173
- def create_flipbook_from_images(images, view_mode="2d", skin="light"):
174
- """Create a flipbook from uploaded images."""
 
 
 
 
175
  session_id = str(uuid.uuid4())
176
- debug_info = []
177
 
178
  if not images:
179
  return (
180
- "<div style='color:red;padding:20px;'>μ΅œμ†Œ ν•œ 개 μ΄μƒμ˜ 이미지λ₯Ό μ—…λ‘œλ“œν•΄μ£Όμ„Έμš”.</div>",
181
- "No images uploaded",
182
  )
183
 
184
  try:
185
- image_paths = [img.name for img in images]
186
- debug_info.append(f"Image paths: {image_paths}")
187
 
188
- pages_info = process_images(image_paths, session_id)
189
- debug_info.append(f"Number of images processed: {len(pages_info)}")
190
 
191
  if not pages_info:
192
- raise RuntimeError("이미지 처리 κ²°κ³Όκ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
193
 
194
- iframe_html = generate_flipbook_html(pages_info, session_id, view_mode, skin)
195
- return iframe_html, "\n".join(debug_info)
 
 
196
 
197
  except Exception as e:
198
  tb = traceback.format_exc()
199
  logging.error(tb)
200
- debug_info.append("❌ ERROR ↓↓↓")
201
- debug_info.append(tb)
202
  return (
203
- f"<div style='color:red;padding:20px;'>였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {e}</div>",
204
- "\n".join(debug_info),
205
  )
206
 
207
 
208
- def generate_flipbook_html(pages_info, session_id, view_mode, skin):
209
- """Generate a standalone HTML file for the flipbook and return link HTML."""
210
- for page in pages_info:
211
- if page.get("htmlContent") is None:
212
- page.pop("htmlContent", None)
213
- if page.get("items") is None:
214
- page.pop("items", None)
215
-
216
- pages_json = json.dumps(pages_info)
217
- html_filename = f"flipbook_{session_id}.html"
218
- html_path = os.path.join(HTML_DIR, html_filename)
219
-
220
- html_content = f"""
221
- <!DOCTYPE html>
222
- <html lang=\"ko\">
223
- <head>
224
- <meta charset=\"UTF-8\">
225
- <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
226
- <title>3D ν”Œλ¦½λΆ</title>
227
- <link rel=\"stylesheet\" type=\"text/css\" href=\"../flipbook.css\">
228
- <style>
229
- body, html {{ margin: 0; padding: 0; height: 100%; overflow: hidden; }}
230
- #flipbook-container {{ width: 100%; height: 100%; position: absolute; top: 0; left: 0; }}
231
- .loading {{ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; font-family: Arial, sans-serif; }}
232
- .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; }}
233
- @keyframes spin {{ 0% {{ transform: rotate(0deg); }} 100% {{ transform: rotate(360deg); }} }}
234
- </style>
235
- <script src=\"../flipbook.js\"></script>
236
- <script src=\"../flipbook.webgl.js\"></script>
237
- <script src=\"../flipbook.swipe.js\"></script>
238
- <script src=\"../flipbook.scroll.js\"></script>
239
- <script src=\"../flipbook.book3.js\"></script>
240
- </head>
241
- <body>
242
- <div id=\"flipbook-container\"></div>
243
- <div id=\"loading\" class=\"loading\">
244
- <div class=\"spinner\"></div>
245
- <div>ν”Œλ¦½λΆ λ‘œλ”© 쀑...</div>
246
- </div>
247
- <script>
248
- document.addEventListener('DOMContentLoaded', function () {{
249
- function hideLoading() {{ document.getElementById('loading').style.display = 'none'; }}
250
- try {{
251
- const options = {{
252
- pages: {pages_json},
253
- viewMode: '{view_mode}',
254
- skin: '{skin}',
255
- responsiveView: true,
256
- singlePageMode: false,
257
- singlePageModeIfMobile: true,
258
- pageFlipDuration: 1,
259
- sound: true,
260
- backgroundMusic: false,
261
- thumbnailsOnStart: true,
262
- btnThumbs: {{ enabled: true }},
263
- btnPrint: {{ enabled: true }},
264
- btnDownloadPages: {{ enabled: true }},
265
- btnDownloadPdf: {{ enabled: true }},
266
- btnShare: {{ enabled: true }},
267
- btnSound: {{ enabled: true }},
268
- btnExpand: {{ enabled: true }},
269
- rightToLeft: false,
270
- autoplayOnStart: false,
271
- autoplayInterval: 3000,
272
- }};
273
- const container = document.getElementById('flipbook-container');
274
- if (container) {{
275
- new FlipBook(container, options);
276
- setTimeout(hideLoading, 1000);
277
- }} else {{
278
- console.error('Flipbook container not found');
279
- alert('였λ₯˜: ν”Œλ¦½λΆ μ»¨ν…Œμ΄λ„ˆλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.');
280
- }}
281
- }} catch (error) {{
282
- console.error('Error initializing flipbook:', error);
283
- alert('ν”Œλ¦½λΆ μ΄ˆκΈ°ν™” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: ' + error.message);
284
- document.getElementById('loading').innerHTML = '<div>였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.</div>';
285
- }}
286
- }});
287
- </script>
288
- </body>
289
- </html>
290
- """
291
-
292
- with open(html_path, "w", encoding="utf-8") as f:
293
- f.write(html_content)
294
-
295
- public_url = f"/public/flipbooks/{html_filename}"
296
- link_html = f"""
297
- <div style=\"text-align:center; padding:20px; background-color:#f9f9f9; border-radius:5px; margin-bottom:20px;\">
298
- <h2 style=\"margin-top:0; color:#333;\">ν”Œλ¦½λΆμ΄ μ€€λΉ„λ˜μ—ˆμŠ΅λ‹ˆλ‹€!</h2>
299
- <p style=\"margin-bottom:20px;\">μ•„λž˜ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ ν”Œλ¦½λΆμ„ μƒˆ μ°½μ—μ„œ μ—΄μ–΄λ³΄μ„Έμš”.</p>
300
- <a href=\"{public_url}\" target=\"_blank\" style=\"display:inline-block; background-color:#4CAF50; color:white; padding
301
-
302
- <div style="text-align:center; padding:20px; background-color:#f9f9f9; border-radius:5px; margin-bottom:20px;">
303
- <h2 style="margin-top:0; color:#333;">ν”Œλ¦½λΆμ΄ μ€€λΉ„λ˜μ—ˆμŠ΅λ‹ˆλ‹€!</h2>
304
- <p style="margin-bottom:20px;">μ•„λž˜ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ ν”Œλ¦½λΆμ„ μƒˆ μ°½μ—μ„œ μ—΄μ–΄λ³΄μ„Έμš”.</p>
305
- <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>
306
- </div>
307
-
308
- <div style="margin-top:20px; padding:15px; background-color:#f5f5f5; border-radius:5px; line-height:1.5;">
309
- <h3 style="margin-top:0; color:#333;">μ‚¬μš© 팁:</h3>
310
- <ul style="margin:10px 0; padding-left:20px;">
311
- <li>νŽ˜μ΄μ§€ λͺ¨μ„œλ¦¬λ₯Ό λ“œλž˜κ·Έν•˜μ—¬ λ„˜κΈΈ 수 μžˆμŠ΅λ‹ˆλ‹€.</li>
312
- <li>ν•˜λ‹¨ νˆ΄λ°”μ˜ μ•„μ΄μ½˜μ„ μ‚¬μš©ν•˜μ—¬ λ‹€μ–‘ν•œ κΈ°λŠ₯을 ν™œμš©ν•˜μ„Έμš”.</li>
313
- <li>전체화면 λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ 더 큰 ν™”λ©΄μœΌλ‘œ λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.</li>
314
- </ul>
315
- <div style="margin-top:10px; padding:10px; background-color:#e8f4fd; border-left:4px solid #2196F3; border-radius:2px;">
316
- <strong>μ°Έκ³ :</strong> ν”Œλ¦½λΆμ€ 2D λͺ¨λ“œμ—μ„œ κ°€μž₯ μ•ˆμ •μ μœΌλ‘œ μž‘λ™ν•©λ‹ˆλ‹€.
317
- </div>
318
- </div>
319
-
320
- <div style="margin-top:15px; background-color:#f5f5f5; border-radius:5px; padding:10px;">
321
- <details>
322
- <summary style="cursor:pointer; color:#2196F3; font-weight:bold;">기술적 세뢀사항 (개발자용)</summary>
323
- <div style="margin-top:10px;">
324
- <p>μ„Έμ…˜ ID: {session_id}</p>
325
- <p>HTML 파일 경둜: {html_path}</p>
326
- <p>νŽ˜μ΄μ§€ 수: {len(pages_info)}</p>
327
- <p>λ·° λͺ¨λ“œ: {view_mode}</p>
328
- <p>μŠ€ν‚¨: {skin}</p>
329
- </div>
330
- </details>
331
- </div>
332
- """
333
-
334
- return link_html
335
-
336
- # Define the Gradio interface
337
  with gr.Blocks(title="3D Flipbook Viewer") as demo:
338
- gr.Markdown("# 3D Flipbook Viewer")
339
- gr.Markdown("""
340
- ## 3D ν”Œλ¦½λΆ λ·°μ–΄
341
-
342
- PDF νŒŒμΌμ΄λ‚˜ μ—¬λŸ¬ 이미지λ₯Ό μ—…λ‘œλ“œν•˜μ—¬ μΈν„°λž™ν‹°λΈŒ 3D ν”Œλ¦½λΆμ„ λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€.
343
-
344
- ### νŠΉμ§•:
345
- - νŽ˜μ΄μ§€ λ„˜κΉ€ νš¨κ³Όμ™€ ν•¨κ»˜ μΈν„°λž™ν‹°λΈŒν•œ κΈ°λŠ₯ 제곡
346
- - 첫 νŽ˜μ΄μ§€μ—λŠ” μ˜ˆμ‹œλ‘œ μΈν„°λž™ν‹°λΈŒ μš”μ†Œκ°€ 포함됨
347
- - νˆ΄λ°”λ₯Ό μ‚¬μš©ν•˜κ±°λ‚˜ νŽ˜μ΄μ§€ λͺ¨μ„œλ¦¬λ₯Ό λ“œλž˜κ·Έν•˜μ—¬ 탐색
348
- - 썸넀일 보기둜 λΉ λ₯Έ 탐색 κ°€λŠ₯
349
- - 전체 ν™”λ©΄μœΌλ‘œ μ „ν™˜ν•˜μ—¬ 더 λ‚˜μ€ 보기 κ²½ν—˜
350
- """)
351
-
352
  with gr.Tabs():
 
353
  with gr.TabItem("PDF μ—…λ‘œλ“œ"):
354
- pdf_file = gr.File(label="PDF 파일 μ—…λ‘œλ“œ", file_types=[".pdf"])
355
-
356
  with gr.Accordion("κ³ κΈ‰ μ„€μ •", open=False):
357
- pdf_view_mode = gr.Radio(
358
- choices=["webgl", "3d", "2d", "swipe"],
359
- value="2d", # Changed default to 2d for better compatibility
360
  label="λ·° λͺ¨λ“œ",
361
- info="WebGL: 졜고 ν’ˆμ§ˆ, 2D: κ°€μž₯ μ•ˆμ •μ , 3D: 쀑간, Swipe: λͺ¨λ°”μΌμš©"
362
  )
363
  pdf_skin = gr.Radio(
364
- choices=["light", "dark", "gradient"],
365
- value="light",
366
  label="μŠ€ν‚¨",
367
- info="light: 밝은 ν…Œλ§ˆ, dark: μ–΄λ‘μš΄ ν…Œλ§ˆ, gradient: κ·ΈλΌλ°μ΄μ…˜ ν…Œλ§ˆ"
368
  )
369
-
370
- pdf_create_btn = gr.Button("PDFμ—μ„œ ν”Œλ¦½λΆ λ§Œλ“€κΈ°", variant="primary", size="lg")
371
- pdf_debug = gr.Textbox(label="디버그 정보", visible=False)
372
- pdf_output = gr.HTML(label="ν”Œλ¦½λΆ κ²°κ³Όλ¬Ό")
373
-
374
- # Set up PDF event handler
375
- pdf_create_btn.click(
376
- fn=create_flipbook_from_pdf,
377
- inputs=[pdf_file, pdf_view_mode, pdf_skin],
378
- outputs=[pdf_output, pdf_debug]
379
  )
380
-
 
381
  with gr.TabItem("이미지 μ—…λ‘œλ“œ"):
382
- images = gr.File(label="이미지 파일 μ—…λ‘œλ“œ", file_types=["image"], file_count="multiple")
383
-
 
 
 
384
  with gr.Accordion("κ³ κΈ‰ μ„€μ •", open=False):
385
- img_view_mode = gr.Radio(
386
- choices=["webgl", "3d", "2d", "swipe"],
387
- value="2d", # Changed default to 2d for better compatibility
388
  label="λ·° λͺ¨λ“œ",
389
- info="WebGL: 졜고 ν’ˆμ§ˆ, 2D: κ°€μž₯ μ•ˆμ •μ , 3D: 쀑간, Swipe: λͺ¨λ°”μΌμš©"
390
  )
391
  img_skin = gr.Radio(
392
- choices=["light", "dark", "gradient"],
393
- value="light",
394
  label="μŠ€ν‚¨",
395
- info="light: 밝은 ν…Œλ§ˆ, dark: μ–΄λ‘μš΄ ν…Œλ§ˆ, gradient: κ·ΈλΌλ°μ΄μ…˜ ν…Œλ§ˆ"
396
  )
397
-
398
- img_create_btn = gr.Button("μ΄λ―Έμ§€μ—μ„œ ν”Œλ¦½λΆ λ§Œλ“€κΈ°", variant="primary", size="lg")
399
- img_debug = gr.Textbox(label="���버그 정보", visible=False)
400
- img_output = gr.HTML(label="ν”Œλ¦½λΆ κ²°κ³Όλ¬Ό")
401
-
402
- # Set up image event handler
403
- img_create_btn.click(
404
- fn=create_flipbook_from_images,
405
- inputs=[images, img_view_mode, img_skin],
406
- outputs=[img_output, img_debug]
407
  )
408
-
409
- gr.Markdown("""
410
- ### μ‚¬μš©λ²•:
411
- 1. 컨텐츠 μœ ν˜•μ— 따라 탭을 μ„ νƒν•˜μ„Έμš” (PDF λ˜λŠ” 이미지)
412
- 2. νŒŒμΌμ„ μ—…λ‘œλ“œν•˜μ„Έμš”
413
- 3. ν•„μš”μ— 따라 κ³ κΈ‰ μ„€μ •μ—μ„œ λ·° λͺ¨λ“œμ™€ μŠ€ν‚¨μ„ μ‘°μ •ν•˜μ„Έμš”
414
- 4. ν”Œλ¦½λΆ λ§Œλ“€κΈ° λ²„νŠΌμ„ ν΄λ¦­ν•˜μ„Έμš”
415
- 5. 좜λ ₯ μ˜μ—­μ—μ„œ ν”Œλ¦½λΆκ³Ό μƒν˜Έμž‘μš©ν•˜μ„Έμš”
416
-
417
- ### μ°Έκ³ :
418
- - 처음 νŽ˜μ΄μ§€μ—λŠ” μ˜ˆμ‹œλ‘œ μΈν„°λž™ν‹°λΈŒ μš”μ†Œμ™€ 링크가 ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€
419
- - μ΅œμƒμ˜ κ²°κ³Όλ₯Ό μœ„ν•΄ μ„ λͺ…ν•œ ν…μŠ€νŠΈμ™€ 이미지가 μžˆλŠ” PDFλ₯Ό μ‚¬μš©ν•˜μ„Έμš”
420
- - μ§€μ›λ˜λŠ” 이미지 ν˜•μ‹: JPG, PNG, GIF λ“±
421
- - ν”Œλ¦½λΆμ΄ 보이지 μ•ŠλŠ” 경우, 2D λͺ¨λ“œλ₯Ό μ„ νƒν•˜κ³  λ‹€μ‹œ μ‹œλ„ν•΄λ³΄μ„Έμš”
422
- """)
423
-
424
-
425
- # Launch the app
426
- if __name__ == "__main__":
427
- demo.launch() # Remove share=True as it's not supported in Spaces
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 3D Flipbook Viewer (Gradio) – 전체 μ†ŒμŠ€
5
+ μ΅œμ’… μˆ˜μ •: 2025-05-18
6
+ """
7
+
8
+ # ────────────────────────────
9
+ # κΈ°λ³Έ λͺ¨λ“ˆ
10
+ # ────────────────────────────
11
  import os
 
12
  import shutil
13
  import uuid
 
14
  import json
15
  import logging
16
  import traceback
17
+ from pathlib import Path
18
+
19
+ # μ™ΈλΆ€ 라이브러리
20
+ import gradio as gr
21
  from PIL import Image
22
+ import fitz # PyMuPDF
23
 
24
+ # ────────────────────────────
25
+ # λ‘œκΉ… μ„€μ •
26
+ # ────────────────────────────
27
  logging.basicConfig(
28
+ level=logging.INFO, # ν•„μš”ν•˜λ©΄ DEBUG
29
  format="%(asctime)s [%(levelname)s] %(message)s",
30
+ filename="app.log", # 동일 디렉터리에 둜그 파일 생성
31
  filemode="a",
32
  )
33
  logging.info("πŸš€ Flipbook app started")
34
 
35
+ # ────────────────────────────
36
+ # μƒμˆ˜ / 경둜
37
+ # ────────────────────────────
38
+ TEMP_DIR = "temp"
39
  UPLOAD_DIR = os.path.join(TEMP_DIR, "uploads")
40
  OUTPUT_DIR = os.path.join(TEMP_DIR, "output")
41
  THUMBS_DIR = os.path.join(OUTPUT_DIR, "thumbs")
42
+ HTML_DIR = os.path.join("public", "flipbooks") # μ›ΉμœΌλ‘œ λ…ΈμΆœλ˜λŠ” μœ„μΉ˜
 
 
 
 
43
 
44
+ # 디렉터리 보μž₯
45
+ for d in [TEMP_DIR, UPLOAD_DIR, OUTPUT_DIR, THUMBS_DIR, HTML_DIR]:
46
+ os.makedirs(d, exist_ok=True)
47
 
48
+ # ────────────────────────────
49
+ # μœ ν‹Έ ν•¨μˆ˜
50
+ # ────────────────────────────
51
+ def create_thumbnail(src: str, dst: str, size=(300, 300)) -> str | None:
52
+ """원본 이미지λ₯Ό μΈλ„€μΌλ‘œ μ €μž₯"""
53
  try:
54
+ with Image.open(src) as im:
55
+ im.thumbnail(size, Image.LANCZOS)
56
+ im.save(dst)
57
+ return dst
58
  except Exception as e:
59
+ logging.error("Thumbnail error: %s", e)
60
  return None
61
 
62
 
63
+ # ────────────────────────────
64
+ # PDF β†’ 이미지
65
+ # ────────────────────────────
66
+ def process_pdf(pdf_path: str, session_id: str) -> list[dict]:
67
  pages_info = []
68
+ out_dir = os.path.join(OUTPUT_DIR, session_id)
69
+ th_dir = os.path.join(THUMBS_DIR, session_id)
70
+ os.makedirs(out_dir, exist_ok=True)
71
+ os.makedirs(th_dir, exist_ok=True)
 
72
 
73
  try:
74
+ pdf_doc = fitz.open(pdf_path)
75
+ for idx, page in enumerate(pdf_doc):
76
+ pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) # 2Γ— 해상도
77
+ img_path = os.path.join(out_dir, f"page_{idx+1}.png")
78
+ pix.save(img_path)
 
79
 
80
+ thumb_path = os.path.join(th_dir, f"thumb_{idx+1}.png")
81
+ create_thumbnail(img_path, thumb_path)
82
 
83
+ html_overlay = (
84
+ """
85
+ <div style="position:absolute;top:50px;left:50px;
86
+ background:rgba(255,255,255,.7);padding:10px;
87
+ border-radius:5px;">
88
+ <div style="font-size:18px;font-weight:bold;color:#333;">
89
+ μΈν„°λž™ν‹°λΈŒ ν”Œλ¦½λΆ 예제
90
+ </div>
91
+ <div style="margin-top:5px;color:#666;">
92
+ 이 νŽ˜μ΄μ§€λŠ” μΈν„°λž™ν‹°λΈŒ 컨텐츠 κΈ°λŠ₯을 λ³΄μ—¬μ€λ‹ˆλ‹€.
93
+ </div>
94
  </div>
95
+ """
96
+ if idx == 0 else None
97
+ )
 
 
 
 
 
 
98
 
99
+ pages_info.append(
100
+ {
101
+ "src": f"./temp/output/{session_id}/page_{idx+1}.png",
102
+ "thumb": f"./temp/output/thumbs/{session_id}/thumb_{idx+1}.png",
103
+ "title": f"νŽ˜μ΄μ§€ {idx+1}",
104
+ "htmlContent": html_overlay,
105
+ }
106
+ )
107
+ logging.info("PDF page %d β†’ %s", idx + 1, img_path)
108
  return pages_info
109
+
110
  except Exception as e:
111
+ logging.error("process_pdf() failed: %s", e)
112
  return []
113
 
114
 
115
+ # ────────────────────────────
116
+ # 이미지 μ—…λ‘œλ“œ 처리
117
+ # ────────────────────────────
118
+ def process_images(img_paths: list[str], session_id: str) -> list[dict]:
119
  pages_info = []
120
+ out_dir = os.path.join(OUTPUT_DIR, session_id)
121
+ th_dir = os.path.join(THUMBS_DIR, session_id)
122
+ os.makedirs(out_dir, exist_ok=True)
123
+ os.makedirs(th_dir, exist_ok=True)
 
124
 
125
+ for i, src in enumerate(img_paths):
126
  try:
127
+ dst = os.path.join(out_dir, f"image_{i+1}.png")
128
+ shutil.copy(src, dst)
129
 
130
+ thumb = os.path.join(th_dir, f"thumb_{i+1}.png")
131
+ create_thumbnail(src, thumb)
132
 
133
  if i == 0:
134
+ html_overlay = """
135
+ <div style="position:absolute;top:50px;left:50px;
136
+ background:rgba(255,255,255,.7);padding:10px;
137
+ border-radius:5px;">
138
+ <div style="font-size:18px;font-weight:bold;color:#333;">
139
+ 이미지 가러리
140
+ </div>
141
+ <div style="margin-top:5px;color:#666;">
142
+ 가러리의 첫 번째 μ΄λ―Έμ§€μž…λ‹ˆλ‹€.
143
  </div>
144
+ </div>
145
  """
146
  elif i == 1:
147
+ html_overlay = """
148
+ <div style="position:absolute;top:50px;left:50px;
149
+ background:rgba(255,255,255,.7);padding:10px;
150
+ border-radius:5px;">
151
+ <div style="font-size:18px;font-weight:bold;color:#333;">
152
+ 두 번째 이��지
153
+ </div>
154
+ <div style="margin-top:5px;color:#666;">
155
+ νŽ˜μ΄μ§€ λͺ¨μ„œλ¦¬λ₯Ό λ“œλž˜κ·Έν•΄ λ„˜κ²¨λ³΄μ„Έμš”.
156
  </div>
157
+ </div>
158
  """
159
  else:
160
+ html_overlay = None
161
+
162
+ pages_info.append(
163
+ {
164
+ "src": f"./temp/output/{session_id}/image_{i+1}.png",
165
+ "thumb": f"./temp/output/thumbs/{session_id}/thumb_{i+1}.png",
166
+ "title": f"이미지 {i+1}",
167
+ "htmlContent": html_overlay,
168
+ }
169
+ )
170
+ logging.info("Image %d copied β†’ %s", i + 1, dst)
171
 
172
  except Exception as e:
173
+ logging.error("process_images() error (%s): %s", src, e)
174
 
175
  return pages_info
176
 
177
 
178
+ # ────────────────────────────
179
+ # ν”Œλ¦½λΆ HTML 생성
180
+ # ────────────────────────────
181
+ def generate_flipbook_html(
182
+ pages_info: list[dict], session_id: str, view_mode: str, skin: str
183
+ ) -> str:
184
+ # None 값은 JSON 직렬화 전에 제거
185
+ for p in pages_info:
186
+ if p.get("htmlContent") is None:
187
+ p.pop("htmlContent", None)
188
+
189
+ pages_json = json.dumps(pages_info, ensure_ascii=False)
190
+ html_file = f"flipbook_{session_id}.html"
191
+ html_path = os.path.join(HTML_DIR, html_file)
192
+
193
+ html = f"""
194
+ <!DOCTYPE html>
195
+ <html lang="ko">
196
+ <head>
197
+ <meta charset="UTF-8">
198
+ <meta name="viewport" content="width=device-width,initial-scale=1">
199
+ <title>3D Flipbook</title>
200
+
201
+ <link rel="stylesheet" href="/public/libs/flipbook/css/flipbook.style.css">
202
+ <script src="/public/libs/flipbook/js/flipbook.min.js"></script>
203
+ <script src="/public/libs/flipbook/js/flipbook.webgl.min.js"></script>
204
+
205
+ <style>
206
+ html,body{{margin:0;height:100%;overflow:hidden}}
207
+ #flipbook-container{{position:absolute;inset:0}}
208
+ .loading{{position:absolute;top:50%;left:50%;
209
+ transform:translate(-50%,-50%);text-align:center;font-family:sans-serif}}
210
+ .spinner{{width:50px;height:50px;border:5px solid #f3f3f3;
211
+ border-top:5px solid #3498db;border-radius:50%;
212
+ animation:spin 1s linear infinite;margin:0 auto 20px}}
213
+ @keyframes spin{{0%{{transform:rotate(0)}}100%{{transform:rotate(360deg)}}}}
214
+ </style>
215
+ </head>
216
+ <body>
217
+ <div id="flipbook-container"></div>
218
+ <div id="loading" class="loading">
219
+ <div class="spinner"></div>
220
+ <div>ν”Œλ¦½λΆ λ‘œλ”© 쀑...</div>
221
+ </div>
222
+
223
+ <script>
224
+ document.addEventListener('DOMContentLoaded',()=>{
225
+ const hide=()=>{{document.getElementById('loading').style.display='none'}};
226
+ try{{
227
+ const options={{pages:{pages_json},
228
+ viewMode:"{view_mode}",
229
+ skin:"{skin}",
230
+ responsiveView:true,
231
+ singlePageMode:false,
232
+ singlePageModeIfMobile:true,
233
+ pageFlipDuration:1,
234
+ thumbnailsOnStart:true,
235
+ btnThumbs:{{enabled:true}},
236
+ btnPrint:{{enabled:true}},
237
+ btnDownloadPages:{{enabled:true}},
238
+ btnDownloadPdf:{{enabled:true}},
239
+ btnShare:{{enabled:true}},
240
+ btnSound:{{enabled:true}},
241
+ btnExpand:{{enabled:true}} }};
242
+ new FlipBook(document.getElementById('flipbook-container'),options);
243
+ setTimeout(hide,1000);
244
+ }}catch(e){{console.error(e);alert('ν”Œλ¦½λΆ μ΄ˆκΈ°ν™” 였λ₯˜:'+e.message);}}
245
+ });
246
+ </script>
247
+ </body></html>
248
+ """
249
+
250
+ Path(html_path).write_text(html, encoding="utf-8")
251
+ public_url = f"/public/flipbooks/{html_file}"
252
+
253
+ # μ‚¬μš©μžμ—κ²Œ λŒλ €μ€„ 링크 덩어리
254
+ return f"""
255
+ <div style="text-align:center;padding:20px;background:#f9f9f9;border-radius:5px">
256
+ <h2 style="margin:0;color:#333">ν”Œλ¦½λΆμ΄ μ€€λΉ„λ˜μ—ˆμŠ΅λ‹ˆλ‹€!</h2>
257
+ <p style="margin:15px 0">λ²„νŠΌμ„ 눌러 μƒˆ μ°½μ—μ„œ ν™•μΈν•˜μ„Έμš”.</p>
258
+ <a href="{public_url}" target="_blank"
259
+ style="display:inline-block;background:#4caf50;color:#fff;
260
+ padding:12px 24px;border-radius:4px;font-weight:bold;font-size:16px">
261
+ ν”Œλ¦½λΆ μ—΄κΈ°
262
+ </a>
263
+ </div>
264
+ """
265
+
266
+
267
+ # ────────────────────────────
268
+ # 콜백: PDF μ—…λ‘œλ“œ
269
+ # ────────────────────────────
270
+ def create_flipbook_from_pdf(
271
+ pdf_file: gr.File | None, view_mode="2d", skin="light"
272
+ ):
273
  session_id = str(uuid.uuid4())
274
+ debug: list[str] = []
275
 
276
  if not pdf_file:
277
  return (
278
+ "<div style='color:red;padding:20px;'>PDF νŒŒμΌμ„ μ—…λ‘œλ“œν•˜μ„Έμš”.</div>",
279
+ "No file",
280
  )
281
 
282
  try:
283
  pdf_path = pdf_file.name
284
+ debug.append(f"PDF path: {pdf_path}")
285
 
 
286
  pages_info = process_pdf(pdf_path, session_id)
287
+ debug.append(f"Extracted pages: {len(pages_info)}")
288
 
289
  if not pages_info:
290
  raise RuntimeError("PDF 처리 κ²°κ³Όκ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
291
 
292
+ html_block = generate_flipbook_html(
293
+ pages_info, session_id, view_mode, skin
294
+ )
295
+ return html_block, "\n".join(debug)
296
 
297
  except Exception as e:
298
  tb = traceback.format_exc()
299
  logging.error(tb)
300
+ debug.extend(["❌ ERROR ↓↓↓", tb])
 
301
  return (
302
+ f"<div style='color:red;padding:20px;'>였λ₯˜: {e}</div>",
303
+ "\n".join(debug),
304
  )
305
 
306
 
307
+ # ────────────────────────────
308
+ # 콜백: 이미지 μ—…λ‘œλ“œ
309
+ # ────────────────────────────
310
+ def create_flipbook_from_images(
311
+ images: list[gr.File] | None, view_mode="2d", skin="light"
312
+ ):
313
  session_id = str(uuid.uuid4())
314
+ debug: list[str] = []
315
 
316
  if not images:
317
  return (
318
+ "<div style='color:red;padding:20px;'>이미지λ₯Ό ν•˜λ‚˜ 이상 μ—…λ‘œλ“œν•˜μ„Έμš”.</div>",
319
+ "No images",
320
  )
321
 
322
  try:
323
+ img_paths = [f.name for f in images]
324
+ debug.append(f"Images: {img_paths}")
325
 
326
+ pages_info = process_images(img_paths, session_id)
327
+ debug.append(f"Processed: {len(pages_info)}")
328
 
329
  if not pages_info:
330
+ raise RuntimeError("이미지 처리 μ‹€νŒ¨")
331
 
332
+ html_block = generate_flipbook_html(
333
+ pages_info, session_id, view_mode, skin
334
+ )
335
+ return html_block, "\n".join(debug)
336
 
337
  except Exception as e:
338
  tb = traceback.format_exc()
339
  logging.error(tb)
340
+ debug.extend(["❌ ERROR ↓↓↓", tb])
 
341
  return (
342
+ f"<div style='color:red;padding:20px;'>였λ₯˜: {e}</div>",
343
+ "\n".join(debug),
344
  )
345
 
346
 
347
+ # ────────────────────────────
348
+ # Gradio UI
349
+ # ────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  with gr.Blocks(title="3D Flipbook Viewer") as demo:
351
+ gr.Markdown("# 3D Flipbook Viewer\nPDF λ˜λŠ” 이미지λ₯Ό μ—…λ‘œλ“œν•΄ μΈν„°λž™ν‹°λΈŒ ν”Œλ¦½λΆμ„ λ§Œλ“œμ„Έμš”.")
352
+
 
 
 
 
 
 
 
 
 
 
 
 
353
  with gr.Tabs():
354
+ # PDF νƒ­
355
  with gr.TabItem("PDF μ—…λ‘œλ“œ"):
356
+ pdf_file = gr.File(label="PDF 파일", file_types=[".pdf"])
 
357
  with gr.Accordion("κ³ κΈ‰ μ„€μ •", open=False):
358
+ pdf_view = gr.Radio(
359
+ ["webgl", "3d", "2d", "swipe"],
360
+ value="2d",
361
  label="λ·° λͺ¨λ“œ",
 
362
  )
363
  pdf_skin = gr.Radio(
364
+ ["light", "dark", "gradient"],
365
+ value="light",
366
  label="μŠ€ν‚¨",
 
367
  )
368
+ pdf_btn = gr.Button("PDF β†’ ν”Œλ¦½λΆ", variant="primary")
369
+ pdf_out = gr.HTML()
370
+ pdf_dbg = gr.Textbox(label="디버그", lines=10)
371
+
372
+ pdf_btn.click(
373
+ create_flipbook_from_pdf,
374
+ inputs=[pdf_file, pdf_view, pdf_skin],
375
+ outputs=[pdf_out, pdf_dbg],
 
 
376
  )
377
+
378
+ # 이미지 νƒ­
379
  with gr.TabItem("이미지 μ—…λ‘œλ“œ"):
380
+ imgs = gr.File(
381
+ label="이미지 νŒŒμΌλ“€",
382
+ file_types=["image"],
383
+ file_count="multiple",
384
+ )
385
  with gr.Accordion("κ³ κΈ‰ μ„€μ •", open=False):
386
+ img_view = gr.Radio(
387
+ ["webgl", "3d", "2d", "swipe"],
388
+ value="2d",
389
  label="λ·° λͺ¨λ“œ",
 
390
  )
391
  img_skin = gr.Radio(
392
+ ["light", "dark", "gradient"],
393
+ value="light",
394
  label="μŠ€ν‚¨",
 
395
  )
396
+ img_btn = gr.Button("이미지 β†’ ν”Œλ¦½λΆ", variant="primary")
397
+ img_out = gr.HTML()
398
+ img_dbg = gr.Textbox(label="디버그", lines=10)
399
+
400
+ img_btn.click(
401
+ create_flipbook_from_images,
402
+ inputs=[imgs, img_view, img_skin],
403
+ outputs=[img_out, img_dbg],
 
 
404
  )
405
+
406
+ gr.Markdown(
407
+ "### μ‚¬μš©λ²•\n"
408
+ "1. PDF λ˜λŠ” 이미지 탭을 μ„ νƒν•˜κ³  νŒŒμΌμ„ μ—…λ‘œλ“œν•©λ‹ˆλ‹€.\n"
409
+ "2. ν•„μš”ν•˜λ©΄ λ·° λͺ¨λ“œ/μŠ€ν‚¨μ„ λ°”κΏ‰λ‹ˆλ‹€.\n"
410
+ "3. β€˜ν”Œλ¦½λΆβ€™ λ²„νŠΌμ„ λˆ„λ₯΄λ©΄ κ²°κ³Όκ°€ μ•„λž˜ λœΉλ‹ˆλ‹€."
411
+ )
412
+
413
+ # ────────────────────────────
414
+ # μ‹€ν–‰
415
+ # ────────────────────────────
416
+ if __name__ == \"__main__\":
417
+ demo.launch(debug=True) # share=True ν•„μš” μ‹œ μΆ”κ°€