vishakhad30 commited on
Commit
d5089aa
Β·
verified Β·
1 Parent(s): 0c2a078

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +4 -6
  2. app.py +797 -0
  3. gitattributes +35 -0
README.md CHANGED
@@ -1,14 +1,12 @@
1
  ---
2
- title: ImageResizer
3
- emoji: 🐒
4
- colorFrom: yellow
5
  colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.19.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
- short_description: Resize images, convert them to JPG, and download them easily
12
  ---
13
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Image Editor
3
+ emoji: 🐠
4
+ colorFrom: green
5
  colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 5.37.0
 
8
  app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,797 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
13
+ import time
14
+ import random
15
+ import cloudscraper
16
+ from urllib.parse import urlparse
17
+ from concurrent.futures import ThreadPoolExecutor, as_completed
18
+ from selenium import webdriver
19
+ from selenium.webdriver.chrome.options import Options
20
+ from selenium.webdriver.chrome.service import Service
21
+ from webdriver_manager.chrome import ChromeDriverManager
22
+
23
+ stop_event = threading.Event()
24
+ cpu_cores = os.cpu_count() or 4
25
+
26
+ USER_AGENTS = [
27
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
28
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
29
+ "Mozilla/5.0 (Linux; Android 10)",
30
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X)"
31
+ ]
32
+
33
+ PROXY_LIST = [
34
+ "http://user:pass@proxy1:port",
35
+ "http://user:pass@proxy2:port",
36
+ ]
37
+
38
+ FAILED_LOG_PATH = "failed_downloads.txt"
39
+ BLOCKED_SITES = ["bigw.com.au"]
40
+
41
+ def random_headers(referer):
42
+ """Return a rotating User‑Agent + referer header dict."""
43
+ return {
44
+ "User-Agent": random.choice(USER_AGENTS),
45
+ "Referer": referer
46
+ }
47
+
48
+ def try_download(url, headers, proxies):
49
+ """
50
+ Attempt a simple GET through a proxy dict {"http":..., "https":...}.
51
+ Returns the response if status_code==200 and content is image, else None.
52
+ """
53
+ try:
54
+ resp = session.get(url, headers=headers, proxies=proxies, stream=True, timeout=15)
55
+ if resp.status_code == 200 and "image" in resp.headers.get("Content-Type", ""):
56
+ return resp
57
+ except Exception as e:
58
+ print(f"⚠️ Proxy download error ({proxies}): {e}")
59
+ return None
60
+
61
+ session = requests.Session()
62
+ # --- Utility Functions ---
63
+ def process_url_images(data, fmt, w, h):
64
+ return process_and_zip(data, fmt, w, h)
65
+
66
+ def process_uploaded_images(files, fmt, w, h):
67
+ tmp = tempfile.mkdtemp()
68
+ data = []
69
+ for f in files:
70
+ src = str(f)
71
+ dst = os.path.join(tmp, os.path.basename(src))
72
+ shutil.copyfile(src, dst)
73
+ data.append({"url": dst, "name": os.path.splitext(os.path.basename(dst))[0]})
74
+ return process_and_zip(data, fmt, w, h)
75
+
76
+ def process_single_url_image(url, fmt, w, h):
77
+ if not url.strip():
78
+ return [], None, "No URL provided", None
79
+ return process_and_zip([{"url": url.strip(), "name": "single"}], fmt, w, h)
80
+
81
+ handle_process = lambda mode, sd, ups, pu, fmt, w, h: (
82
+ process_url_images(sd, fmt, w, h) if mode.startswith("πŸ“„") and sd else
83
+ process_uploaded_images(ups, fmt, w, h) if mode.startswith("πŸ“€") and ups else
84
+ process_single_url_image(pu, fmt, w, h) if pu.strip() else
85
+ ([], None, "⚠️ No valid input provided", None)
86
+ )
87
+ from PIL import ImageFile
88
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
89
+
90
+ def fetch_with_retries(url, headers, timeout=15, retries=2, backoff=1):
91
+ """
92
+ Try up to retries+1 times to GET url with timeout seconds.
93
+ Returns a requests.Response or None.
94
+ """
95
+ for attempt in range(retries + 1):
96
+ try:
97
+ resp = session.get(url, headers=headers, stream=True, timeout=timeout)
98
+ if resp.status_code == 200 and "image" in resp.headers.get("Content-Type", ""):
99
+ return resp
100
+ except Exception as e:
101
+ print(f"⚠️ Attempt {attempt+1} failed for {url}: {e}")
102
+ time.sleep(backoff * (attempt + 1))
103
+ return None
104
+
105
+ def download_image(url, save_path):
106
+ # Skip cached
107
+ if os.path.exists(save_path) and os.path.getsize(save_path) > 1000:
108
+ return save_path
109
+
110
+ # Skip blocked
111
+ if any(b in url for b in BLOCKED_SITES):
112
+ log_failure(url, "Skipped known slow site")
113
+ return None
114
+
115
+ referer = f"{urlparse(url).scheme}://{urlparse(url).netloc}/"
116
+ headers = random_headers(referer)
117
+
118
+ # 1) Direct with retries
119
+ print(f"πŸš€ Fetching with retries: {url}")
120
+ resp = fetch_with_retries(url, headers, timeout=15, retries=2)
121
+ if resp:
122
+ with open(save_path, 'wb') as f:
123
+ for chunk in resp.iter_content(8192):
124
+ f.write(chunk)
125
+ print("βœ… Direct fetch succeeded")
126
+ return save_path
127
+ else:
128
+ print("❌ Direct fetch failed after retries")
129
+
130
+ # 2) cloudscraper fallback
131
+ print("🟠 cloudscraper fallback...")
132
+ try:
133
+ scraper = cloudscraper.create_scraper(sess=session)
134
+ resp = scraper.get(url, headers=headers, stream=True, timeout=15)
135
+ if resp.status_code == 200 and "image" in resp.headers.get("Content-Type", ""):
136
+ with open(save_path, 'wb') as f:
137
+ for chunk in resp.iter_content(8192):
138
+ f.write(chunk)
139
+ print("βœ… cloudscraper worked")
140
+ return save_path
141
+ except Exception as e:
142
+ print(f"❌ cloudscraper failed: {e}")
143
+
144
+ # 3) Selenium fallback
145
+ try:
146
+ print("πŸ§ͺ Headless browser fallback...")
147
+ chrome_opts = Options()
148
+ chrome_opts.add_argument("--headless")
149
+ chrome_opts.add_argument("--disable-gpu")
150
+ chrome_opts.add_argument("--no-sandbox")
151
+ chrome_opts.add_argument("--disable-dev-shm-usage")
152
+ chrome_opts.add_argument(f"user-agent={headers['User-Agent']}")
153
+
154
+ driver = webdriver.Chrome(
155
+ service=Service(ChromeDriverManager().install()),
156
+ options=chrome_opts
157
+ )
158
+ driver.get(url)
159
+ time.sleep(4)
160
+ final_url = driver.current_url
161
+ cookies = {c['name']: c['value'] for c in driver.get_cookies()}
162
+ driver.quit()
163
+
164
+ with session:
165
+ for k, v in cookies.items():
166
+ session.cookies.set(k, v)
167
+ r = session.get(final_url, headers=headers, stream=True, timeout=15)
168
+ if r.status_code == 200 and "image" in r.headers.get("Content-Type", ""):
169
+ with open(save_path, 'wb') as f:
170
+ for chunk in r.iter_content(8192):
171
+ f.write(chunk)
172
+ print("βœ… Selenium + cookies worked")
173
+ return save_path
174
+ except Exception as e:
175
+ print(f"❌ Selenium failed: {e}")
176
+
177
+ # 4) Give up
178
+ log_failure(url, "All methods failed")
179
+ return None
180
+
181
+
182
+ lamp_html = """
183
+ <style>
184
+ * {
185
+ margin: 0;
186
+ padding: 0;
187
+ box-sizing: border-box;
188
+ }
189
+ body {
190
+ background-color: #D8D2C2;
191
+ transition: background 0.4s ease;
192
+ }
193
+ body.night {
194
+ background: radial-gradient(#555, #111);
195
+ }
196
+ .lamp-fixed {
197
+ position: fixed;
198
+ top: 0;
199
+ right: 0;
200
+ width: 100px;
201
+ height: 100vh;
202
+ display: flex;
203
+ flex-direction: column;
204
+ align-items: center;
205
+ z-index: 9999;
206
+ pointer-events: none;
207
+ }
208
+ .mount {
209
+ width: 30px;
210
+ height: 5px;
211
+ background: #111;
212
+ border-radius: 6px;
213
+ pointer-events: none;
214
+ }
215
+ .wire {
216
+ width: 4px;
217
+ height: 120px;
218
+ background: #000;
219
+ margin: 0;
220
+ pointer-events: none;
221
+ }
222
+ .blub {
223
+ position: relative;
224
+ width: 47px;
225
+ height: 47px;
226
+ background: #444;
227
+ border-radius: 50%;
228
+ cursor: pointer;
229
+ margin-top: -1px;
230
+ transition: all 0.3s ease;
231
+ z-index: 2;
232
+ pointer-events: auto;
233
+ }
234
+ .blub.night {
235
+ background: #ffec99;
236
+ box-shadow: 0 0 28px #ffec99, 0 0 56px #ffec99,
237
+ 0 0 84px #ffec99, 0 0 168px #ffec99;
238
+ }
239
+ .blub.night::before {
240
+ background: #ffec99;
241
+ }
242
+ .blub.night::after {
243
+ content: "";
244
+ position: absolute;
245
+ top: 50%;
246
+ left: 50%;
247
+ transform: translate(-60%, -50%);
248
+ width: 78px;
249
+ height: 78px;
250
+ background: #ffec99;
251
+ border-radius: 100%;
252
+ filter: blur(26px);
253
+ z-index: -1;
254
+ }
255
+ .blub::before {
256
+ content: "";
257
+ position: absolute;
258
+ top: -30px;
259
+ left: 11px;
260
+ width: 25px;
261
+ height: 47px;
262
+ background: #444;
263
+ border-top: 19px solid #000;
264
+ border-radius: 8px;
265
+ transition: background 0.3s ease;
266
+ }
267
+ .blub span:nth-child(1) {
268
+ position: absolute;
269
+ top: -10px;
270
+ left: -2px;
271
+ width: 20px;
272
+ height: 18px;
273
+ background: transparent;
274
+ transform: rotate(343deg);
275
+ border-bottom-right-radius: 40px;
276
+ box-shadow: 12px 12px 0 5px #444;
277
+ transition: box-shadow 0.3s ease;
278
+ }
279
+ .blub.night span:nth-child(1) {
280
+ box-shadow: 12px 12px 0 5px #ffec99;
281
+ }
282
+ .blub span:nth-child(2) {
283
+ position: absolute;
284
+ top: -10px;
285
+ right: -2px;
286
+ width: 20px;
287
+ height: 18px;
288
+ background: transparent;
289
+ transform: rotate(18deg);
290
+ border-bottom-left-radius: 50px;
291
+ box-shadow: -12px 12px 0 5px #444;
292
+ transition: box-shadow 0.3s ease;
293
+ }
294
+ .blub.night span:nth-child(2) {
295
+ box-shadow: -12px 12px 0 5px #ffec99;
296
+ }
297
+ </style>
298
+ <div class="lamp-fixed">
299
+ <div class="mount"></div>
300
+ <div class="wire"></div>
301
+ <div class="blub" onclick="this.classList.toggle('night'); document.body.classList.toggle('night')">
302
+ <span></span>
303
+ <span></span>
304
+ </div>
305
+ </div>
306
+ """
307
+ def resize_with_padding(img, target_w, target_h, fill=(255,255,255)):
308
+ img.thumbnail((target_w, target_h), Image.LANCZOS)
309
+ bg = Image.new("RGB", (target_w, target_h), fill)
310
+ x = (target_w - img.width) // 2
311
+ y = (target_h - img.height) // 2
312
+ bg.paste(img, (x, y))
313
+ return bg
314
+
315
+ def threaded_download_and_open(item, temp_dir):
316
+ from PIL import ImageSequence
317
+
318
+ name, src = item['name'], item['url']
319
+ try:
320
+ if os.path.exists(src):
321
+ path = src
322
+ else:
323
+ temp_path = os.path.join(temp_dir, f"{name}.gif")
324
+ path = download_image(src, temp_path)
325
+ if not path:
326
+ return (name, None, 'Download failed or invalid image')
327
+
328
+ img = Image.open(path)
329
+
330
+ # Rewind to first frame in case it's not
331
+ if getattr(img, "is_animated", False):
332
+ img.seek(0)
333
+ frames = []
334
+ for frame in ImageSequence.Iterator(img):
335
+ frame_copy = frame.convert("RGBA").copy()
336
+ frames.append(frame_copy)
337
+ return (name, frames, None)
338
+ else:
339
+ return (name, [img.convert("RGBA")], None)
340
+
341
+ except Exception as e:
342
+ return (name, None, str(e))
343
+
344
+ def resize_and_save_worker(args):
345
+ name, img, fmt, w, h, out_dir = args
346
+ try:
347
+ # 1) Composite RGBA over white
348
+ img = img.convert('RGBA')
349
+ bg = Image.new('RGBA', img.size, (255,255,255,255))
350
+ img = Image.alpha_composite(bg, img)
351
+
352
+ # 2) Determine save format & extension
353
+ fmt_upper = fmt.upper()
354
+ save_fmt = "JPEG" if fmt_upper in ("JPEG", "JPG") else fmt_upper
355
+ ext = "jpg" if fmt_upper in ("JPEG", "JPG") else fmt.lower()
356
+
357
+ # 3) Convert to RGB if needed
358
+ if save_fmt == "JPEG":
359
+ img = img.convert("RGB")
360
+
361
+ # 4) Resize with padding
362
+ img = resize_with_padding(img, w, h)
363
+
364
+ # 5) Build filename & save
365
+ out_name = f"{name}.{ext}"
366
+ out_path = os.path.join(out_dir, out_name)
367
+ img.save(out_path, format=save_fmt, quality=90)
368
+
369
+ return out_path, None
370
+
371
+ except Exception as e:
372
+ return None, f"{name}: {e}"
373
+
374
+ from multiprocessing import cpu_count
375
+ import zipfile
376
+
377
+ def process_and_zip(items, fmt, w, h):
378
+ tmp = tempfile.mkdtemp()
379
+ proc = os.path.join(tmp, 'out')
380
+ os.makedirs(proc, exist_ok=True)
381
+
382
+ files, fails = [], []
383
+ # 1) Download & open in parallel
384
+ workers = cpu_count() * 2
385
+ with ThreadPoolExecutor(max_workers=workers) as ex:
386
+ results = list(ex.map(lambda it: threaded_download_and_open(it, tmp), items))
387
+
388
+ # 2) Process each downloaded image/list of frames
389
+ for name, imgs, err in results:
390
+ if err or not imgs:
391
+ fails.append(f"{name}: {err}")
392
+ continue
393
+
394
+ for i, img in enumerate(imgs):
395
+ try:
396
+ # Composite RGBA over white
397
+ img = img.convert("RGBA")
398
+ bg = Image.new("RGBA", img.size, (255,255,255,255))
399
+ img = Image.alpha_composite(bg, img)
400
+
401
+ # Determine save format & extension
402
+ fmt_upper = fmt.upper()
403
+ save_fmt = "JPEG" if fmt_upper in ("JPEG","JPG") else fmt_upper
404
+ ext = "jpg" if fmt_upper in ("JPEG","JPG") else fmt.lower()
405
+
406
+ # Convert to RGB if JPEG
407
+ if save_fmt == "JPEG":
408
+ img = img.convert("RGB")
409
+
410
+ # Resize with padding
411
+ img = resize_with_padding(img, w, h)
412
+
413
+ # Build filename
414
+ fname = (
415
+ f"{name}_frame{i+1}.{ext}"
416
+ if len(imgs) > 1
417
+ else f"{name}.{ext}"
418
+ )
419
+ out_path = os.path.join(proc, fname)
420
+
421
+ # Save & record
422
+ img.save(out_path, format=save_fmt, quality=90)
423
+ files.append(out_path)
424
+
425
+ except Exception as e:
426
+ print(f"⚠️ Save failed for {name}: {e}")
427
+ fails.append(f"{name}_frame{i+1}: {e}")
428
+
429
+ # 3) If nothing succeeded, clean up and report
430
+ if not files:
431
+ stop_event.clear()
432
+ shutil.rmtree(tmp)
433
+ return None, None, "No images processed.", None
434
+
435
+ # 4) Create ZIP in β€œstore” mode (no compression)
436
+ date_str = datetime.now().strftime("%Y-%m-%d")
437
+ zip_name = f"{date_str}.zip"
438
+ zip_path = os.path.join(tmp, zip_name)
439
+ with zipfile.ZipFile(zip_path, 'w', compression=zipfile.ZIP_STORED) as zf:
440
+ for f in files:
441
+ zf.write(f, os.path.basename(f))
442
+
443
+ # 5) Build status message
444
+ msg_lines = [f"βœ… Processed {len(files)} image(s)."]
445
+ if fails:
446
+ msg_lines.append(f"❌ Failed: {len(fails)} image(s)")
447
+ msg_lines += [f" - {fail}" for fail in fails]
448
+
449
+ stop_event.clear()
450
+ return files, zip_path, "\n".join(msg_lines), tmp
451
+
452
+ def read_uploaded_workbook(file):
453
+ if not file:
454
+ return [], "❌ No file uploaded"
455
+ try:
456
+ # read all sheets except "Cleared Data"
457
+ xls = pd.ExcelFile(file.name)
458
+ sheets = [s for s in xls.sheet_names if s.lower() != "cleared data"]
459
+ df_list = [pd.read_excel(file.name, sheet_name=s, engine="openpyxl") for s in sheets]
460
+ df = pd.concat(df_list, ignore_index=True)
461
+ df.columns = [c.strip() for c in df.columns]
462
+
463
+ item_col = next((c for c in df.columns if c.lower() == 'itemcode'), None)
464
+ if not item_col:
465
+ return [], "❌ Missing 'ItemCode' column"
466
+
467
+ url_cols = [c for c in df.columns if any(k in c.lower() for k in ["url", "image", "link"])]
468
+ data = []
469
+ for _, row in df.iterrows():
470
+ raw = row[item_col]
471
+ if pd.isna(raw):
472
+ continue
473
+ key = str(raw).strip().split('.')[0] if str(raw).strip().replace('.', '', 1).isdigit() else str(raw).strip()
474
+ idx = 0
475
+ for col in url_cols:
476
+ if pd.notna(row[col]):
477
+ name = f"{key}" if idx == 0 else f"{key}_{idx}"
478
+ data.append({"url": str(row[col]).strip(), "name": name})
479
+ idx += 1
480
+ return data, f"βœ… Fetched {len(data)} image link(s)"
481
+ except Exception as e:
482
+ return [], f"❌ Error: {e}"
483
+
484
+ def clear_all(tmp_dir):
485
+ # wipe out the temp folder if it exists
486
+ if tmp_dir and os.path.exists(tmp_dir):
487
+ shutil.rmtree(tmp_dir)
488
+ return (
489
+ None, # clear the single‐file upload
490
+ [], # reset the image_data_state
491
+ [], # clear the multi‐file upload
492
+ gr.update(visible=False, value=None), # hide & clear the hidden ZIP file
493
+ [], # clear the β€œindividual downloads” list
494
+ "Cleared.", # status message
495
+ None, # reset temp_dir_state
496
+ "" # clear the URL textbox
497
+ )
498
+
499
+
500
+ # === CSS ===
501
+ css = """
502
+ body {
503
+ margin: 0;
504
+ background: #4B352A;
505
+ font-family: 'Segoe UI', sans-serif;
506
+ color: #222;
507
+ display: flex;
508
+ justify-content: center;
509
+ padding: 2rem;
510
+ transition: background 0.3s, color 0.3s;
511
+ }
512
+ .gradio-container {
513
+ max-width: 1200px; /* You can reduce this (e.g., 600px) */
514
+ width: 100%;
515
+ margin: 0 auto;
516
+ h1 {
517
+ font-size: 24px !important;
518
+ font-weight: 700 !important;
519
+ margin-bottom: 1rem !important;
520
+ color: #FFFFFF !important;
521
+ text-align: center !important;
522
+ }
523
+ .panel {
524
+ background: white;
525
+ padding: 1rem;
526
+ border-radius: 6px;
527
+ border: 1px solid #ddd;
528
+ box-shadow: 0 1px 2px rgba(0,0,0,0.05);
529
+ }
530
+ #clear-btn, #stop-btn {
531
+ margin-top: 1rem;
532
+ font-size: 0.85rem !important;
533
+ font-weight: 600 !important;
534
+ padding: 0.5rem 1rem !important;
535
+ border-radius: 8px !important;
536
+ border: none !important;
537
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
538
+ transition: background-color 0.2s ease !important;
539
+ width: 100% !important;
540
+ }
541
+ .btn-row {
542
+ display: flex;
543
+ justify-content: space-between;
544
+ gap: 0.5rem;
545
+ }
546
+ #clear-btn {
547
+ background-color: #e74c3c !important;
548
+ color: white !important;
549
+ width: 50% !important;
550
+ }
551
+ #clear-btn:hover {
552
+ background-color: #c0392b !important;
553
+ cursor: pointer !important;
554
+ }
555
+ #stop-btn {
556
+ background-color: #CA7842 !important;
557
+ color: white !important;
558
+ width: 50% !important;
559
+ }
560
+ #stop-btn:hover {
561
+ background-color: #FFA55D !important;
562
+ cursor: pointer !important;
563
+ }
564
+ .gradio-container .gr-row > div {
565
+ padding: 4px !important;
566
+ }
567
+ #process-btn-url {
568
+ background-color: #A4B465 !important;
569
+ color: #333 !important;
570
+ }
571
+ #fetch-btn {
572
+ background-color: #cfbed7 !important;
573
+ color: #333 !important;
574
+ }
575
+ #status-box {
576
+ background-color: #ffffff;
577
+ border: 2px solid #cfbed7;
578
+ border-radius: 6px;
579
+ padding: 1rem;
580
+ font-size: 0.95rem;
581
+ color: #333;
582
+ /* Styling the toggle radio buttons */
583
+ .gradio-container label.svelte-1ipelgc {
584
+ font-weight: 600 !important;
585
+ border: 2px solid #A4B465 !important;
586
+ padding: 0.4rem 1rem !important;
587
+ border-radius: 12px !important;
588
+ margin: 0.25rem !important;
589
+ background-color: #f5fbe8 !important;
590
+ transition: all 0.3s ease-in-out;
591
+ cursor: pointer;
592
+ }
593
+ /* Selected option */
594
+ input[type="radio"]:checked + label.svelte-1ipelgc {
595
+ background-color: #A4B465 !important;
596
+ color: white !important;
597
+ border-color: #889E46 !important;
598
+ }
599
+ /* Unselected hover */
600
+ .gradio-container input[type="radio"] + label.svelte-1ipelgc:hover {
601
+ background-color: #e0f1c4 !important;
602
+ }
603
+ }
604
+ """
605
+ gr.HTML("""
606
+ <div class="wrapper">
607
+ <div class="toggle-icon-group">
608
+ <input type="radio" id="spreadsheet" name="mode" checked>
609
+ <label for="spreadsheet"><span>πŸ“„</span> Spreadsheet</label>
610
+ <input type="radio" id="upload" name="mode">
611
+ <label for="upload"><span>πŸ“</span> Upload</label>
612
+ </div>
613
+ </div>
614
+ """)
615
+
616
+ def process_all(mode, workbook_file, ups, pu, fmt, w, h):
617
+ # 1️⃣ If workbook mode, read links from Excel
618
+ if mode.startswith("πŸ“„"):
619
+ data, msg = read_uploaded_workbook(workbook_file)
620
+ print("πŸ’‘ DEBUG – links extracted:", data) # <<< DEBUG
621
+ if not data:
622
+ # no links or error reading
623
+ return [], None, msg, None
624
+
625
+ # we have a list of {"url":…, "name":…}
626
+ files, zip_path, proc_msg, tmp = process_and_zip(data, fmt, w, h)
627
+ return files, zip_path, f"{msg}\n{proc_msg}", tmp
628
+
629
+ # 2️⃣ If β€œUpload Images” mode
630
+ if mode.startswith("πŸ“€") and ups:
631
+ return process_uploaded_images(ups, fmt, w, h)
632
+
633
+ # 3️⃣ If single‑URL mode
634
+ if pu and pu.strip():
635
+ return process_single_url_image(pu, fmt, w, h)
636
+
637
+ # 4️⃣ Nothing valid
638
+ return [], None, "⚠️ No valid input provided", None
639
+
640
+ if os.path.exists(FAILED_LOG_PATH):
641
+ with open(FAILED_LOG_PATH) as f:
642
+ failed = [line.split("\t")[1] for line in f if line.strip()]
643
+ if failed:
644
+ print(f"πŸ”„ Retrying {len(failed)} failed URLs…")
645
+ # reuse your ThreadPoolExecutor to download them to a β€œretries” folder
646
+ from concurrent.futures import ThreadPoolExecutor, as_completed
647
+ os.makedirs("retries", exist_ok=True)
648
+ def retry_one(url):
649
+ fname = os.path.join("retries", url.split("/")[-1].split("?")[0])
650
+ r = fetch_with_retries(url, random_headers(f"{urlparse(url).scheme}://{urlparse(url).netloc}/"), timeout=15, retries=2)
651
+ if r:
652
+ with open(fname, 'wb') as f:
653
+ for c in r.iter_content(8192): f.write(c)
654
+ return True
655
+ return False
656
+
657
+ with ThreadPoolExecutor(max_workers=8) as ex:
658
+ results = list(ex.map(retry_one, failed))
659
+ print(f"βœ… Retried successes: {sum(results)} / {len(failed)}")
660
+
661
+
662
+ with gr.Blocks(css=css) as demo:
663
+ # ─── States ───
664
+ image_data_state = gr.State([])
665
+ temp_dir_state = gr.State(None)
666
+
667
+ # ─── Header ───
668
+ gr.HTML(lamp_html)
669
+ with gr.Row():
670
+ gr.Markdown("<h1>πŸ–ΌοΈ Image Processor</h1>")
671
+
672
+ # ─── Main layout ───
673
+ with gr.Row():
674
+ # ── LEFT PANEL ──
675
+ with gr.Column(scale=3, min_width=500):
676
+ with gr.Group(elem_classes="panel"):
677
+ mode_toggle = gr.Radio(
678
+ ["πŸ“„ Upload Workbook", "πŸ“€ Upload Images"],
679
+ value="πŸ“„ Upload Workbook",
680
+ label="Select Input Method"
681
+ )
682
+ workbook_upload = gr.File(
683
+ label="πŸ“‚ Upload .xlsx/.xlsm Workbook",
684
+ file_types=['.xlsx', '.xlsm'],
685
+ visible=True
686
+ )
687
+ upload_box = gr.File(
688
+ label="πŸ“ Upload Images",
689
+ file_count="multiple",
690
+ visible=False
691
+ )
692
+ image_url_input = gr.Textbox(
693
+ label="🌐 Paste Image URL",
694
+ visible=False
695
+ )
696
+ process_btn = gr.Button("βš™οΈ Process", elem_id="process-btn-url")
697
+
698
+ with gr.Group(elem_classes="panel"):
699
+ with gr.Row():
700
+ format_choice = gr.Dropdown(
701
+ ["JPG","PNG","WEBP","TIFF","GIF","JFIF","AVIF"],
702
+ label="πŸ–ΌοΈ Format",
703
+ value="JPG"
704
+ )
705
+ width = gr.Number(label="Width (px)", value=1000, precision=0)
706
+ height = gr.Number(label="Height (px)", value=1000, precision=0)
707
+
708
+ # ── RIGHT PANEL ──
709
+ with gr.Column(scale=2, min_width=400):
710
+ with gr.Group(elem_classes="panel"):
711
+ status = gr.Textbox(
712
+ label="πŸ“£ Status",
713
+ lines=6,
714
+ interactive=False,
715
+ elem_id="status-box"
716
+ )
717
+ zip_download_btn = gr.Button("πŸ“¦ Download ZIP")
718
+ zip_file_hidden = gr.File(visible=False)
719
+ with gr.Accordion("🧷 Individual Files", open=False):
720
+ single_downloads = gr.File(label="Files", file_count="multiple")
721
+
722
+ with gr.Row(elem_classes="btn-row"):
723
+ stop_btn = gr.Button("Stop", elem_id="stop-btn")
724
+ clear_btn = gr.Button("Clear", elem_id="clear-btn")
725
+
726
+ # ─── Footer ───
727
+ gr.HTML(
728
+ "<div style='width:100%; text-align:center; margin-top:1rem; color:white;'>"
729
+ "Created with πŸ’œ by Vishakha"
730
+ "</div>"
731
+ )
732
+
733
+ # ─── EVENT HOOKS ───
734
+ mode_toggle.change(
735
+ fn=lambda choice: (
736
+ gr.update(visible=choice.startswith("πŸ“„")),
737
+ gr.update(visible=not choice.startswith("πŸ“„")),
738
+ gr.update(visible=not choice.startswith("πŸ“„"))
739
+ ),
740
+ inputs=[mode_toggle],
741
+ outputs=[workbook_upload, upload_box, image_url_input]
742
+ )
743
+
744
+ workbook_upload.change(
745
+ fn=read_uploaded_workbook,
746
+ inputs=[workbook_upload],
747
+ outputs=[image_data_state, status]
748
+ )
749
+
750
+ upload_box.change(
751
+ fn=lambda files: f"{len(files)} files ready." if files else "No files selected",
752
+ inputs=[upload_box],
753
+ outputs=[status]
754
+ )
755
+
756
+ process_btn.click(
757
+ fn=process_all,
758
+ inputs=[
759
+ mode_toggle,
760
+ workbook_upload,
761
+ upload_box,
762
+ image_url_input,
763
+ format_choice,
764
+ width,
765
+ height
766
+ ],
767
+ outputs=[
768
+ single_downloads,
769
+ zip_file_hidden,
770
+ status,
771
+ temp_dir_state
772
+ ]
773
+ )
774
+
775
+ zip_download_btn.click(
776
+ None,
777
+ inputs=[zip_file_hidden],
778
+ js="(file) => file && window.open(file.url, '_blank')"
779
+ )
780
+
781
+ clear_btn.click(
782
+ fn=clear_all,
783
+ inputs=[temp_dir_state],
784
+ outputs=[
785
+ workbook_upload, image_data_state, upload_box,
786
+ zip_file_hidden, single_downloads, status,
787
+ temp_dir_state, image_url_input
788
+ ]
789
+ )
790
+
791
+ stop_btn.click(
792
+ fn=lambda: "πŸ›‘ Stop signal sent",
793
+ outputs=[status]
794
+ )
795
+
796
+ if __name__ == "__main__":
797
+ demo.queue().launch(debug=True)
gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text