ginipick commited on
Commit
0c676dc
Β·
verified Β·
1 Parent(s): 1daf82e

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +302 -0
app.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.responses import HTMLResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ import pathlib, os, uvicorn, base64
5
+
6
+ BASE = pathlib.Path(__file__).parent
7
+ app = FastAPI()
8
+ app.mount("/static", StaticFiles(directory=BASE), name="static")
9
+
10
+ # PDF 디렉토리 μ„€μ •
11
+ PDF_DIR = BASE / "pdf"
12
+ if not PDF_DIR.exists():
13
+ PDF_DIR.mkdir(parents=True)
14
+
15
+ # PDF 파일 λͺ©λ‘ κ°€μ Έμ˜€κΈ°
16
+ def get_pdf_files():
17
+ pdf_files = []
18
+ if PDF_DIR.exists():
19
+ pdf_files = [f for f in PDF_DIR.glob("*.pdf")]
20
+ return pdf_files
21
+
22
+ # PDF 썸넀일 생성 및 ν”„λ‘œμ νŠΈ 데이터 μ€€λΉ„
23
+ def generate_pdf_projects():
24
+ projects_data = []
25
+ pdf_files = get_pdf_files()
26
+
27
+ for pdf_file in pdf_files:
28
+ projects_data.append({
29
+ "path": str(pdf_file),
30
+ "name": pdf_file.stem
31
+ })
32
+
33
+ return projects_data
34
+
35
+ HTML = """
36
+ <!doctype html><html lang="ko"><head>
37
+ <meta charset="utf-8"><title>FlipBook Space</title>
38
+ <link rel="stylesheet" href="/static/flipbook.css">
39
+ <script src="/static/three.js"></script>
40
+ <script src="/static/iscroll.js"></script>
41
+ <script src="/static/mark.js"></script>
42
+ <script src="/static/mod3d.js"></script>
43
+ <script src="/static/pdf.js"></script>
44
+ <script src="/static/flipbook.js"></script>
45
+ <script src="/static/flipbook.book3.js"></script>
46
+ <script src="/static/flipbook.scroll.js"></script>
47
+ <script src="/static/flipbook.swipe.js"></script>
48
+ <script src="/static/flipbook.webgl.js"></script>
49
+ <style>
50
+ body{margin:0;background:#f0f0f0;font-family:sans-serif}
51
+ header{max-width:960px;margin:0 auto;padding:18px 20px;display:flex;align-items:center}
52
+ #homeBtn{display:none;width:38px;height:38px;border:none;border-radius:50%;cursor:pointer;
53
+ background:#0077c2;color:#fff;font-size:20px;margin-right:12px}
54
+ #homeBtn:hover{background:#005999}
55
+ h2{margin:0;font-size:1.5rem;font-weight:600}
56
+ #home,#viewerPage{max-width:960px;margin:0 auto;padding:0 20px 40px}
57
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,180px);gap:16px;margin-top:24px}
58
+ .card{
59
+ background:#fff url('/static/book2.jpg') no-repeat center center;
60
+ background-size: cover;
61
+ border:1px solid #ccc;
62
+ border-radius:6px;
63
+ cursor:pointer;
64
+ box-shadow:0 2px 4px rgba(0,0,0,.12);
65
+ width: 180px;
66
+ height: 240px;
67
+ position: relative;
68
+ display: flex;
69
+ flex-direction: column;
70
+ align-items: center;
71
+ justify-content: center;
72
+ }
73
+ .card img{
74
+ width:75%;
75
+ height:auto;
76
+ object-fit:contain;
77
+ position:relative;
78
+ display:block;
79
+ margin:0 auto;
80
+ border: 1px solid #ddd;
81
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
82
+ }
83
+ .card p{
84
+ text-align:center;
85
+ margin:6px 0;
86
+ background: rgba(255, 255, 255, 0.7);
87
+ padding: 4px 8px;
88
+ border-radius: 4px;
89
+ }
90
+ button.upload{all:unset;cursor:pointer;border:1px solid #bbb;padding:8px 14px;border-radius:6px;background:#fff;margin:0 8px}
91
+ #viewer{width:100%;max-width:1200px;height:80vh;margin:24px auto;background:#fff;border:1px solid #ccc}
92
+ </style></head><body>
93
+
94
+ <header>
95
+ <button id="homeBtn" title="ν™ˆμœΌλ‘œ">βŒ‚</button>
96
+ <h2>My FlipBook Projects</h2>
97
+ </header>
98
+
99
+ <section id="home">
100
+ <div>
101
+ <label class="upload">πŸ“· 이미지 <input id="imgInput" type="file" accept="image/*" multiple hidden></label>
102
+ <label class="upload">πŸ“„ PDF <input id="pdfInput" type="file" accept="application/pdf" hidden></label>
103
+ </div>
104
+ <div class="grid" id="grid"></div>
105
+ </section>
106
+
107
+ <section id="viewerPage" style="display:none">
108
+ <div id="viewer"></div>
109
+ </section>
110
+
111
+ <script>
112
+ let projects=[], fb=null;
113
+ const grid=$id('grid'), viewer=$id('viewer');
114
+ pdfjsLib.GlobalWorkerOptions.workerSrc='/static/pdf.worker.js';
115
+
116
+ // μ„œλ²„μ—μ„œ 미리 λ‘œλ“œλœ PDF ν”„λ‘œμ νŠΈ
117
+ let serverProjects = [];
118
+
119
+ /* πŸ”Š μ˜€λ””μ˜€ unlock – λ‚΄μž₯ Audio 와 같은 MP3 경둜 μ‚¬μš© */
120
+ ['click','touchstart'].forEach(evt=>{
121
+ document.addEventListener(evt,function u(){new Audio('static/turnPage2.mp3')
122
+ .play().then(a=>a.pause()).catch(()=>{});document.removeEventListener(evt,u,{capture:true});},
123
+ {once:true,capture:true});
124
+ });
125
+
126
+ /* ── μœ ν‹Έ ── */
127
+ function $id(id){return document.getElementById(id)}
128
+ function addCard(i,thumb,title){
129
+ const d=document.createElement('div');
130
+ d.className='card';
131
+ d.onclick=()=>open(i);
132
+ d.innerHTML=`<img src="${thumb}"><p>${title || 'ν”„λ‘œμ νŠΈ ' + (i+1)}</p>`;
133
+ grid.appendChild(d);
134
+ }
135
+
136
+ /* ── 이미지 μ—…λ‘œλ“œ ── */
137
+ $id('imgInput').onchange=e=>{
138
+ const files=[...e.target.files]; if(!files.length) return;
139
+ const pages=[],tot=files.length;let done=0;
140
+ files.forEach((f,i)=>{const r=new FileReader();r.onload=x=>{pages[i]={src:x.target.result,thumb:x.target.result};
141
+ if(++done===tot) save(pages);};r.readAsDataURL(f);});
142
+ };
143
+
144
+ /* ── PDF μ—…λ‘œλ“œ ── */
145
+ $id('pdfInput').onchange=e=>{
146
+ const file=e.target.files[0]; if(!file) return;
147
+ const fr=new FileReader();
148
+ fr.onload=v=>{
149
+ pdfjsLib.getDocument({data:v.target.result}).promise.then(async pdf=>{
150
+ const pages=[];
151
+ for(let p=1;p<=pdf.numPages;p++){
152
+ const pg=await pdf.getPage(p), vp=pg.getViewport({scale:1});
153
+ const c=document.createElement('canvas');c.width=vp.width;c.height=vp.height;
154
+ await pg.render({canvasContext:c.getContext('2d'),viewport:vp}).promise;
155
+ pages.push({src:c.toDataURL(),thumb:c.toDataURL()});
156
+ }
157
+ save(pages, file.name.replace('.pdf', ''));
158
+ });
159
+ };fr.readAsArrayBuffer(file);
160
+ };
161
+
162
+ /* ── ν”„λ‘œμ νŠΈ μ €μž₯ ── */
163
+ function save(pages, title){
164
+ const id=projects.push(pages)-1;
165
+ addCard(id,pages[0].thumb, title);
166
+ }
167
+
168
+ /* ── μ„œλ²„ PDF λ‘œλ“œ ── */
169
+ async function loadServerPDFs() {
170
+ try {
171
+ const response = await fetch('/api/pdf-projects');
172
+ serverProjects = await response.json();
173
+
174
+ // μ„œλ²„ PDF λ‘œλ“œ 및 썸넀일 생성
175
+ for(let i = 0; i < serverProjects.length; i++) {
176
+ const project = serverProjects[i];
177
+ const response = await fetch(`/api/pdf-thumbnail?path=${encodeURIComponent(project.path)}`);
178
+ const data = await response.json();
179
+
180
+ if(data.thumbnail) {
181
+ const pages = [{
182
+ src: data.thumbnail,
183
+ thumb: data.thumbnail,
184
+ path: project.path
185
+ }];
186
+
187
+ save(pages, project.name);
188
+ }
189
+ }
190
+ } catch(error) {
191
+ console.error('μ„œλ²„ PDF λ‘œλ“œ μ‹€νŒ¨:', error);
192
+ }
193
+ }
194
+
195
+ /* ── μΉ΄λ“œ β†’ FlipBook ── */
196
+ function open(i){
197
+ toggle(false);
198
+ const pages = projects[i];
199
+
200
+ // 둜컬 ν”„λ‘œμ νŠΈ λ˜λŠ” μ„œλ²„ PDF λ‘œλ“œ
201
+ if(fb){fb.destroy();viewer.innerHTML='';}
202
+
203
+ if(pages[0].path) {
204
+ // μ„œλ²„ PDF 파일 λ‘œλ“œ
205
+ fetch(`/api/pdf-content?path=${encodeURIComponent(pages[0].path)}`)
206
+ .then(response => response.arrayBuffer())
207
+ .then(pdfData => {
208
+ pdfjsLib.getDocument({data: pdfData}).promise.then(async pdf => {
209
+ const pdfPages = [];
210
+ for(let p = 1; p <= pdf.numPages; p++) {
211
+ const pg = await pdf.getPage(p), vp = pg.getViewport({scale: 1});
212
+ const c = document.createElement('canvas');
213
+ c.width = vp.width;
214
+ c.height = vp.height;
215
+ await pg.render({canvasContext: c.getContext('2d'), viewport: vp}).promise;
216
+ pdfPages.push({src: c.toDataURL(), thumb: c.toDataURL()});
217
+ }
218
+
219
+ createFlipBook(pdfPages);
220
+ });
221
+ })
222
+ .catch(error => {
223
+ console.error('PDF λ‘œλ“œ μ‹€νŒ¨:', error);
224
+ });
225
+ } else {
226
+ // μ—…λ‘œλ“œλœ ν”„λ‘œμ νŠΈ 보기
227
+ createFlipBook(pages);
228
+ }
229
+ }
230
+
231
+ function createFlipBook(pages) {
232
+ fb = new FlipBook(viewer, {
233
+ pages,
234
+ viewMode: 'webgl',
235
+ autoSize: true,
236
+ flipDuration: 800,
237
+ backgroundColor: '#fff',
238
+ /* πŸ”Š λ‚΄μž₯ μ‚¬μš΄λ“œ */
239
+ sound: true,
240
+ assets: {flipMp3: 'static/turnPage2.mp3', hardFlipMp3: 'static/turnPage2.mp3'},
241
+ controlsProps: {enableFullscreen: true, thumbnails: true}
242
+ });
243
+ }
244
+
245
+ /* ── λ„€λΉ„κ²Œμ΄μ…˜ ── */
246
+ $id('homeBtn').onclick=()=>toggle(true);
247
+ function toggle(showHome){
248
+ $id('home').style.display=showHome?'block':'none';
249
+ $id('viewerPage').style.display=showHome?'none':'block';
250
+ $id('homeBtn').style.display=showHome?'none':'inline-block';
251
+ }
252
+
253
+ // νŽ˜μ΄μ§€ λ‘œλ“œ μ‹œ μ„œλ²„ PDF λ‘œλ“œ
254
+ window.addEventListener('DOMContentLoaded', loadServerPDFs);
255
+ </script>
256
+ </body></html>
257
+ """
258
+
259
+ # API μ—”λ“œν¬μΈνŠΈ: PDF ν”„λ‘œμ νŠΈ λͺ©λ‘
260
+ @app.get("/api/pdf-projects")
261
+ async def get_pdf_projects():
262
+ return generate_pdf_projects()
263
+
264
+ # API μ—”λ“œν¬μΈνŠΈ: PDF 썸넀일 생성
265
+ @app.get("/api/pdf-thumbnail")
266
+ async def get_pdf_thumbnail(path: str):
267
+ try:
268
+ import fitz # PyMuPDF
269
+
270
+ # PDF 파일 μ—΄κΈ°
271
+ doc = fitz.open(path)
272
+
273
+ # 첫 νŽ˜μ΄μ§€ κ°€μ Έμ˜€κΈ°
274
+ if doc.page_count > 0:
275
+ page = doc[0]
276
+ # μΈλ„€μΌμš© 이미지 λ Œλ”λ§ (해상도 μ‘°μ •)
277
+ pix = page.get_pixmap(matrix=fitz.Matrix(0.5, 0.5))
278
+ img_data = pix.tobytes("png")
279
+
280
+ # Base64 인코딩
281
+ b64_img = base64.b64encode(img_data).decode('utf-8')
282
+ return {"thumbnail": f"data:image/png;base64,{b64_img}"}
283
+
284
+ return {"thumbnail": None}
285
+ except Exception as e:
286
+ return {"error": str(e), "thumbnail": None}
287
+
288
+ # API μ—”λ“œν¬μΈνŠΈ: PDF μ½˜ν…μΈ  κ°€μ Έμ˜€κΈ°
289
+ @app.get("/api/pdf-content")
290
+ async def get_pdf_content(path: str):
291
+ try:
292
+ with open(path, "rb") as pdf_file:
293
+ return pdf_file.read()
294
+ except Exception as e:
295
+ return {"error": str(e)}
296
+
297
+ @app.get("/", response_class=HTMLResponse)
298
+ async def root():
299
+ return HTML
300
+
301
+ if __name__ == "__main__":
302
+ uvicorn.run("app:app", host="0.0.0.0", port=int(os.getenv("PORT", 7860)))