Workpam commited on
Commit
368240c
Β·
verified Β·
1 Parent(s): 8931645

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +533 -0
app.py ADDED
@@ -0,0 +1,533 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from PIL import Image
3
+ import os
4
+ import zipfile
5
+ import pandas as pd
6
+ import requests
7
+ import tempfile
8
+ import shutil
9
+ from io import BytesIO
10
+ import threading
11
+ from datetime import datetime
12
+ from concurrent.futures import ThreadPoolExecutor, as_completed
13
+ import time
14
+ import random
15
+ import cloudscraper
16
+ from urllib.parse import urlparse
17
+ from selenium import webdriver
18
+ from selenium.webdriver.chrome.options import Options
19
+ from selenium.webdriver.chrome.service import Service
20
+ from webdriver_manager.chrome import ChromeDriverManage
21
+
22
+ # --- Utility Functions ---
23
+ def process_url_images(data, fmt, w, h):
24
+ return process_and_zip(data, fmt, w, h)
25
+
26
+ def process_uploaded_images(files, fmt, w, h):
27
+ tmp = tempfile.mkdtemp()
28
+ data = []
29
+ for f in files:
30
+ src = str(f)
31
+ dst = os.path.join(tmp, os.path.basename(src))
32
+ shutil.copyfile(src, dst)
33
+ data.append({"url": dst, "name": os.path.splitext(os.path.basename(dst))[0]})
34
+ return process_and_zip(data, fmt, w, h)
35
+
36
+ def process_single_url_image(url, fmt, w, h):
37
+ if not url.strip():
38
+ return [], None, "No URL provided", None
39
+ return process_and_zip([{"url": url.strip(), "name": "single"}], fmt, w, h)
40
+
41
+ handle_process = lambda mode, sd, ups, pu, fmt, w, h: (
42
+ process_url_images(sd, fmt, w, h) if mode.startswith("πŸ“„") and sd else
43
+ process_uploaded_images(ups, fmt, w, h) if mode.startswith("πŸ“€") and ups else
44
+ process_single_url_image(pu, fmt, w, h) if pu.strip() else
45
+ ([], None, "⚠️ No valid input provided", None)
46
+ )
47
+ import cloudscraper
48
+
49
+ def download_image(url, save_path):
50
+ if os.path.exists(save_path) and os.path.getsize(save_path) > 1000:
51
+ print(f"βœ… Skipping cached: {save_path}")
52
+ return save_path
53
+
54
+ if any(blocked in url for blocked in BLOCKED_SITES):
55
+ log_failure(url, "Skipped known slow site")
56
+ return None
57
+
58
+ referer = f"{urlparse(url).scheme}://{urlparse(url).netloc}/"
59
+ headers = random_headers(referer)
60
+
61
+ # 1. Try direct
62
+ try:
63
+ print("πŸš€ Trying direct...")
64
+ resp = session.get(url, headers=headers, stream=True, timeout=8)
65
+ if resp.status_code == 200 and "image" in resp.headers.get("Content-Type", ""):
66
+ with open(save_path, 'wb') as f:
67
+ for chunk in resp.iter_content(8192):
68
+ f.write(chunk)
69
+ print("βœ… Direct worked")
70
+ return save_path
71
+ except Exception as e:
72
+ print(f"⚠️ Direct failed: {e}")
73
+
74
+ # 2. Try proxies in parallel
75
+ if PROXY_LIST:
76
+ headers = random_headers(referer)
77
+ with ThreadPoolExecutor(max_workers=5) as executor:
78
+ futures = {
79
+ executor.submit(try_download, url, headers, {"http": p, "https": p}): p
80
+ for p in random.sample(PROXY_LIST, min(5, len(PROXY_LIST)))
81
+ }
82
+ for future in as_completed(futures):
83
+ result = future.result()
84
+ if result:
85
+ with open(save_path, 'wb') as f:
86
+ for chunk in result.iter_content(8192):
87
+ f.write(chunk)
88
+ print("βœ… Proxy worked:", futures[future])
89
+ return save_path
90
+
91
+ # 3. cloudscraper fallback
92
+ try:
93
+ print("🟠 cloudscraper fallback...")
94
+ scraper = cloudscraper.create_scraper(sess=session)
95
+ resp = scraper.get(url, headers=headers, stream=True, timeout=12)
96
+ if resp.status_code == 200 and "image" in resp.headers.get("Content-Type", ""):
97
+ with open(save_path, 'wb') as f:
98
+ for chunk in resp.iter_content(8192):
99
+ f.write(chunk)
100
+ print("βœ… cloudscraper worked")
101
+ return save_path
102
+ except Exception as e:
103
+ print(f"❌ cloudscraper failed: {e}")
104
+
105
+ # 4. Final fallback: Selenium
106
+ try:
107
+ print("πŸ§ͺ Headless browser fallback...")
108
+ chrome_opts = Options()
109
+ chrome_opts.add_argument("--headless")
110
+ chrome_opts.add_argument("--disable-gpu")
111
+ chrome_opts.add_argument("--no-sandbox")
112
+ chrome_opts.add_argument("--disable-dev-shm-usage")
113
+ chrome_opts.add_argument(f"user-agent={headers['User-Agent']}")
114
+
115
+ driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_opts)
116
+ driver.get(url)
117
+ time.sleep(4)
118
+ final_url = driver.current_url
119
+ cookies = {c['name']: c['value'] for c in driver.get_cookies()}
120
+ driver.quit()
121
+
122
+ with session:
123
+ for k, v in cookies.items():
124
+ session.cookies.set(k, v)
125
+ r = session.get(final_url, headers=headers, stream=True, timeout=15)
126
+ if r.status_code == 200 and 'image' in r.headers.get("Content-Type", ""):
127
+ with open(save_path, 'wb') as f:
128
+ for chunk in r.iter_content(8192):
129
+ f.write(chunk)
130
+ print("βœ… Selenium + cookies worked")
131
+ return save_path
132
+ else:
133
+ log_failure(url, f"Selenium bad response: {r.status_code}")
134
+ except Exception as e:
135
+ print(f"❌ Selenium failed: {e}")
136
+ log_failure(url, f"Selenium exception: {e}")
137
+
138
+ # Fail
139
+ log_failure(url, "All methods failed")
140
+ return None
141
+
142
+ def resize_with_padding(img, target_w, target_h, fill=(255,255,255)):
143
+ img.thumbnail((target_w, target_h), Image.LANCZOS)
144
+ bg = Image.new("RGB", (target_w, target_h), fill)
145
+ x = (target_w - img.width) // 2
146
+ y = (target_h - img.height) // 2
147
+ bg.paste(img, (x, y))
148
+ return bg
149
+
150
+ def threaded_download_and_open(item, temp_dir):
151
+ from PIL import ImageSequence
152
+
153
+ name, src = item['name'], item['url']
154
+ try:
155
+ if os.path.exists(src):
156
+ path = src
157
+ else:
158
+ temp_path = os.path.join(temp_dir, f"{name}.gif")
159
+ path = download_image(src, temp_path)
160
+ if not path:
161
+ return (name, None, 'Download failed or invalid image')
162
+
163
+ img = Image.open(path)
164
+
165
+ # Rewind to first frame in case it's not
166
+ if getattr(img, "is_animated", False):
167
+ img.seek(0)
168
+ frames = []
169
+ for frame in ImageSequence.Iterator(img):
170
+ frame_copy = frame.convert("RGBA").copy()
171
+ frames.append(frame_copy)
172
+ return (name, frames, None)
173
+ else:
174
+ return (name, [img.convert("RGBA")], None)
175
+
176
+ except Exception as e:
177
+ return (name, None, str(e))
178
+
179
+ def resize_and_save_worker(args):
180
+ name, img, fmt, w, h, out_dir = args
181
+ try:
182
+ img = img.convert('RGBA')
183
+ bg = Image.new('RGBA', img.size, (255,255,255,255))
184
+ img = Image.alpha_composite(bg, img)
185
+ if fmt.upper() == 'JPEG':
186
+ img = img.convert('RGB')
187
+ img = resize_with_padding(img, w, h)
188
+ out_name = f"{name}.{fmt.lower()}"
189
+ out_path = os.path.join(out_dir, out_name)
190
+ img.save(out_path, format=fmt.upper(), quality=90)
191
+ return out_path, None
192
+ except Exception as e:
193
+ return None, f"{name}: {e}"
194
+
195
+ def process_and_zip(items, fmt, w, h):
196
+ tmp = tempfile.mkdtemp()
197
+ proc = os.path.join(tmp, 'out')
198
+ os.makedirs(proc, exist_ok=True)
199
+ files, fails = [], []
200
+
201
+ with ThreadPoolExecutor(max_workers=8) as ex:
202
+ results = list(ex.map(lambda it: threaded_download_and_open(it, tmp), items))
203
+
204
+ for name, imgs, err in results:
205
+ if stop_event.is_set():
206
+ print("πŸ›‘ Stopped before processing.")
207
+ break
208
+
209
+ if err or imgs is None:
210
+ fails.append(f"{name}: {err}")
211
+ continue
212
+
213
+ for i, img in enumerate(imgs):
214
+ try:
215
+ img = img.convert('RGBA')
216
+ bg = Image.new('RGBA', img.size, (255, 255, 255, 255))
217
+ img = Image.alpha_composite(bg, img)
218
+
219
+ if fmt.upper() == 'JPEG':
220
+ img = img.convert('RGB')
221
+
222
+ img = resize_with_padding(img, w, h)
223
+
224
+ fname = f"{name}_frame{i+1}.{fmt.lower()}" if len(imgs) > 1 else f"{name}.{fmt.lower()}"
225
+ path = os.path.join(proc, fname)
226
+ img.save(path, format=fmt.upper(), quality=90)
227
+ files.append(path)
228
+ except Exception as e:
229
+ fails.append(f"{name}_frame{i+1}: {e}")
230
+
231
+ if not files:
232
+ stop_event.clear()
233
+ shutil.rmtree(tmp)
234
+ return None, None, "No images processed.", None
235
+
236
+ date_str = datetime.now().strftime("%Y-%m-%d")
237
+ zip_name = f"{date_str}.zip"
238
+ zip_path = os.path.join(tmp, zip_name)
239
+
240
+ with zipfile.ZipFile(zip_path, 'w') as zf:
241
+ for f in files:
242
+ zf.write(f, os.path.basename(f))
243
+
244
+ msg_lines = [f"βœ… Processed {len(files)} image(s)."]
245
+ if fails:
246
+ msg_lines.append(f"❌ Failed: {len(fails)} image(s)")
247
+ msg_lines += [f" - {fail}" for fail in fails]
248
+
249
+ stop_event.clear()
250
+ return files, zip_path, "\n".join(msg_lines), tmp
251
+ def read_uploaded_workbook(file):
252
+ if not file:
253
+ return [], "❌ No file uploaded"
254
+ try:
255
+ # read all sheets except "Cleared Data"
256
+ xls = pd.ExcelFile(file.name)
257
+ sheets = [s for s in xls.sheet_names if s.lower() != "cleared data"]
258
+ df_list = [pd.read_excel(file.name, sheet_name=s, engine="openpyxl") for s in sheets]
259
+ df = pd.concat(df_list, ignore_index=True)
260
+ df.columns = [c.strip() for c in df.columns]
261
+
262
+ item_col = next((c for c in df.columns if c.lower() == 'itemcode'), None)
263
+ if not item_col:
264
+ return [], "❌ Missing 'ItemCode' column"
265
+
266
+ url_cols = [c for c in df.columns if any(k in c.lower() for k in ["url", "image", "link"])]
267
+ data = []
268
+ for _, row in df.iterrows():
269
+ raw = row[item_col]
270
+ if pd.isna(raw):
271
+ continue
272
+ key = str(raw).strip().split('.')[0] if str(raw).strip().replace('.', '', 1).isdigit() else str(raw).strip()
273
+ idx = 0
274
+ for col in url_cols:
275
+ if pd.notna(row[col]):
276
+ name = f"{key}" if idx == 0 else f"{key}_{idx}"
277
+ data.append({"url": str(row[col]).strip(), "name": name})
278
+ idx += 1
279
+ return data, f"βœ… Fetched {len(data)} image link(s)"
280
+ except Exception as e:
281
+ return [], f"❌ Error: {e}"
282
+
283
+ def clear_all(tmp_dir):
284
+ if tmp_dir and os.path.exists(tmp_dir):
285
+ shutil.rmtree(tmp_dir)
286
+ return "", [], [], gr.update(visible=False), [], "Cleared.", None, ""
287
+
288
+ # === CSS ===
289
+ css = """
290
+ body {
291
+ margin: 0;
292
+ background: #4B352A;
293
+ font-family: 'Segoe UI', sans-serif;
294
+ color: #222;
295
+ display: flex;
296
+ justify-content: center;
297
+ padding: 2rem;
298
+ transition: background 0.3s, color 0.3s;
299
+ }
300
+ .gradio-container {
301
+ max-width: 1200px; /* You can reduce this (e.g., 600px) */
302
+ width: 100%;
303
+ margin: 0 auto;
304
+ h1 {
305
+ font-size: 24px !important;
306
+ font-weight: 700 !important;
307
+ margin-bottom: 1rem !important;
308
+ color: #FFFFFF !important;
309
+ text-align: center !important;
310
+ }
311
+ .panel {
312
+ background: white;
313
+ padding: 1rem;
314
+ border-radius: 6px;
315
+ border: 1px solid #ddd;
316
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
317
+ }
318
+ #clear-btn, #stop-btn {
319
+ margin-top: 1rem;
320
+ font-size: 0.85rem !important;
321
+ font-weight: 600 !important;
322
+ padding: 0.5rem 1rem !important;
323
+ border-radius: 8px !important;
324
+ border: none !important;
325
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
326
+ transition: background-color 0.2s ease !important;
327
+ width: 100% !important;
328
+ }
329
+ .btn-row {
330
+ display: flex;
331
+ justify-content: space-between;
332
+ gap: 0.5rem;
333
+ }
334
+ #clear-btn {
335
+ background-color: #e74c3c !important;
336
+ color: white !important;
337
+ width: 50% !important;
338
+ }
339
+ #clear-btn:hover {
340
+ background-color: #c0392b !important;
341
+ cursor: pointer !important;
342
+ }
343
+ #stop-btn {
344
+ background-color: #CA7842 !important;
345
+ color: white !important;
346
+ width: 50% !important;
347
+ }
348
+ #stop-btn:hover {
349
+ background-color: #FFA55D !important;
350
+ cursor: pointer !important;
351
+ }
352
+ .gradio-container .gr-row > div {
353
+ padding: 4px !important;
354
+ }
355
+ #process-btn-url {
356
+ background-color: #A4B465 !important;
357
+ color: #333 !important;
358
+ }
359
+ #fetch-btn {
360
+ background-color: #cfbed7 !important;
361
+ color: #333 !important;
362
+ }
363
+ #status-box {
364
+ background-color: #ffffff;
365
+ border: 2px solid #cfbed7;
366
+ border-radius: 6px;
367
+ padding: 1rem;
368
+ font-size: 0.95rem;
369
+ color: #333;
370
+ /* Styling the toggle radio buttons */
371
+ .gradio-container label.svelte-1ipelgc {
372
+ font-weight: 600 !important;
373
+ border: 2px solid #A4B465 !important;
374
+ padding: 0.4rem 1rem !important;
375
+ border-radius: 12px !important;
376
+ margin: 0.25rem !important;
377
+ background-color: #f5fbe8 !important;
378
+ transition: all 0.3s ease-in-out;
379
+ cursor: pointer;
380
+ }
381
+ /* Selected option */
382
+ input[type="radio"]:checked + label.svelte-1ipelgc {
383
+ background-color: #A4B465 !important;
384
+ color: white !important;
385
+ border-color: #889E46 !important;
386
+ }
387
+ /* Unselected hover */
388
+ .gradio-container input[type="radio"] + label.svelte-1ipelgc:hover {
389
+ background-color: #e0f1c4 !important;
390
+ }
391
+ }
392
+ """
393
+ gr.HTML("""
394
+ <div class="wrapper">
395
+ <div class="toggle-icon-group">
396
+ <input type="radio" id="spreadsheet" name="mode" checked>
397
+ <label for="spreadsheet"><span>πŸ“„</span> Spreadsheet</label>
398
+ <input type="radio" id="upload" name="mode">
399
+ <label for="upload"><span>πŸ“</span> Upload</label>
400
+ </div>
401
+ </div>
402
+ """)
403
+
404
+ with gr.Blocks(css=css) as demo:
405
+ image_data_state = gr.State([])
406
+ temp_dir_state = gr.State(None)
407
+
408
+ gr.HTML(lamp_html)
409
+
410
+ with gr.Row():
411
+ gr.Markdown("<h1>πŸ–ΌοΈ Image Processor</h1>")
412
+
413
+ with gr.Row():
414
+ with gr.Column(scale=3):
415
+ with gr.Group(elem_classes="panel"):
416
+ mode_toggle = gr.Radio(
417
+ ["πŸ“„ Upload Workbook", "πŸ“€ Upload Images"],
418
+ value="πŸ“„ Upload Workbook",
419
+ label="Select Input Method"
420
+ )
421
+
422
+ # NEW: workbook upload replaces URL textbox & fetch button
423
+ workbook_upload = gr.File(
424
+ label="πŸ“‚ Upload .xlsx/.xlsm Workbook",
425
+ file_types=['.xlsx', '.xlsm'],
426
+ visible=True
427
+ )
428
+ status = gr.Textbox(
429
+ label="πŸ“£ Status", lines=6, interactive=False, elem_id="status-box"
430
+ )
431
+
432
+ upload_box = gr.File(
433
+ label="πŸ“ Upload Images", file_count="multiple", visible=False
434
+ )
435
+ image_url_input = gr.Textbox(
436
+ label="🌐 Paste Image URL", visible=False
437
+ )
438
+
439
+ def toggle_inputs(choice):
440
+ is_workbook = choice.startswith("πŸ“„")
441
+ return (
442
+ gr.update(visible=is_workbook), # workbook_upload
443
+ gr.update(visible=not is_workbook),# upload_box
444
+ gr.update(visible=not is_workbook) # image_url_input
445
+ )
446
+
447
+ mode_toggle.change(
448
+ fn=toggle_inputs,
449
+ inputs=[mode_toggle],
450
+ outputs=[workbook_upload, upload_box, image_url_input]
451
+ )
452
+
453
+ # When a workbook is uploaded, immediately parse it
454
+ workbook_upload.change(
455
+ fn=read_uploaded_workbook,
456
+ inputs=[workbook_upload],
457
+ outputs=[image_data_state, status]
458
+ )
459
+
460
+ upload_box.change(
461
+ lambda files: f"{len(files)} files ready." if files else "No files selected",
462
+ [upload_box],
463
+ [status]
464
+ )
465
+
466
+ with gr.Group(elem_classes="panel"):
467
+ format_choice = gr.Dropdown(
468
+ ["JPEG", "PNG", "WEBP", "TIFF", "GIF", "JFIF", "AVIF"],
469
+ label="πŸ–ΌοΈ Format", value="JPEG", scale=1
470
+ )
471
+ width = gr.Number(label=" Width (px)", value=1000, precision=0, scale=1)
472
+ height = gr.Number(label=" Height (px)", value=1000, precision=0, scale=1)
473
+
474
+ with gr.Column(scale=2):
475
+ with gr.Group(elem_classes="panel"):
476
+ zip_download_btn = gr.Button("πŸ“¦ Download ZIP")
477
+ zip_file_hidden = gr.File(visible=False)
478
+ with gr.Accordion("🧷 Individual Files", open=False):
479
+ single_downloads = gr.File(label="Files", file_count="multiple")
480
+
481
+ with gr.Row(elem_classes="btn-row"):
482
+ stop_btn = gr.Button("Stop", elem_id="stop-btn")
483
+ clear_btn = gr.Button("Clear", elem_id="clear-btn")
484
+
485
+ gr.Markdown("<center style='margin-top:1rem;color:white'>Created with πŸ’œ by Vishakha</center>")
486
+
487
+ # processing hook stays unchanged
488
+ process_btn = gr.Button("βš™οΈ Process", elem_id="process-btn-url")
489
+ process_btn.click(
490
+ lambda mode, sd, ups, pu, fmt, w, h: (
491
+ process_url_images(sd, fmt, w, h) if mode.startswith("πŸ“„") and sd else
492
+ process_uploaded_images(ups, fmt, w, h) if mode.startswith("πŸ“€") and ups else
493
+ process_single_url_image(pu, fmt, w, h) if pu.strip() else
494
+ ([], None, "⚠️ No valid input provided", None)
495
+ ),
496
+ inputs=[mode_toggle, image_data_state, upload_box, image_url_input, format_choice, width, height],
497
+ outputs=[single_downloads, zip_file_hidden, status, temp_dir_state]
498
+ )
499
+
500
+ zip_download_btn.click(
501
+ None,
502
+ inputs=[zip_file_hidden],
503
+ js="(file) => { if (file) { window.open(file.url, '_blank'); } }"
504
+ )
505
+
506
+ def clear_all(tmp_dir):
507
+ if tmp_dir and os.path.exists(tmp_dir):
508
+ shutil.rmtree(tmp_dir)
509
+ return "", [], [], gr.update(visible=False), [], "Cleared.", None, ""
510
+
511
+ clear_btn.click(
512
+ clear_all,
513
+ [temp_dir_state],
514
+ [
515
+ workbook_upload, # clear workbook input
516
+ image_data_state, # state
517
+ upload_box, # clear upload images
518
+ zip_file_hidden,
519
+ single_downloads,
520
+ status,
521
+ temp_dir_state,
522
+ image_url_input
523
+ ]
524
+ )
525
+
526
+ def stop_processing():
527
+ stop_event.set()
528
+ return "πŸ›‘ Stop signal sent"
529
+
530
+ stop_btn.click(stop_processing, outputs=[status])
531
+
532
+ if __name__ == "__main__":
533
+ demo.queue().launch(debug=True)