dschandra commited on
Commit
66096c2
·
verified ·
1 Parent(s): c201187

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +37 -567
app.py CHANGED
@@ -1,590 +1,60 @@
1
- import os
2
  import io
3
- import math
4
- from flask import Flask, request, jsonify, send_file, render_template_string
5
  from PIL import Image
6
- from werkzeug.utils import secure_filename
7
 
8
- app = Flask(__name__)
9
- app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB max upload
 
10
 
11
- ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'webp'}
 
 
 
12
 
13
- def allowed_file(filename):
14
- return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
15
 
16
- def get_file_size_label(size_bytes):
17
- if size_bytes < 1024 * 1024:
18
- return f"{size_bytes / 1024:.1f} KB"
19
- return f"{size_bytes / (1024 * 1024):.2f} MB"
20
-
21
- def compress_to_target(img, fmt, target_bytes, original_bytes):
22
- """
23
- Smart compression: reduce file size to target_bytes without visible quality loss.
24
- Strategy:
25
- 1. Try lossless/near-lossless optimization first (subsampling, optimize flags).
26
- 2. If still too large, gently reduce quality in steps.
27
- 3. Never go below quality=75 to avoid visible degradation.
28
- """
29
- buf = io.BytesIO()
30
-
31
- # ---- PNG path (lossless format) ----
32
- if fmt == "PNG":
33
- # PNG compression level 0-9; higher = smaller but slower
34
- for level in range(1, 10):
35
- buf = io.BytesIO()
36
- img.save(buf, format="PNG", optimize=True, compress_level=level)
37
- if buf.tell() <= target_bytes:
38
- buf.seek(0)
39
- return buf, buf.tell(), "PNG"
40
- # If PNG can't reach target, convert to WebP lossless
41
- buf = io.BytesIO()
42
- img.save(buf, format="WEBP", lossless=True, quality=100)
43
- if buf.tell() <= target_bytes:
44
- buf.seek(0)
45
- return buf, buf.tell(), "WEBP"
46
- # Last resort: lossy WebP but keep quality high (>=80)
47
- for q in range(95, 79, -5):
48
  buf = io.BytesIO()
49
- img.save(buf, format="WEBP", quality=q, method=6)
50
  if buf.tell() <= target_bytes:
51
  buf.seek(0)
52
- return buf, buf.tell(), "WEBP"
53
  buf.seek(0)
54
- return buf, buf.tell(), "WEBP"
55
-
56
- # ---- JPEG / WEBP path (lossy formats) ----
57
- save_fmt = "JPEG" if fmt in ("JPEG", "JPG") else "WEBP"
58
-
59
- # First pass: high quality with optimize
60
- for q in range(95, 74, -2):
61
- buf = io.BytesIO()
62
- if save_fmt == "JPEG":
63
- img.save(buf, format="JPEG", quality=q, optimize=True,
64
- subsampling=0 if q >= 85 else 2)
65
- else:
66
- img.save(buf, format="WEBP", quality=q, method=6)
67
- if buf.tell() <= target_bytes:
68
- buf.seek(0)
69
- return buf, buf.tell(), save_fmt
70
-
71
- # Reached minimum quality — return best effort
72
- buf.seek(0)
73
- return buf, buf.tell(), save_fmt
74
-
75
-
76
- HTML_PAGE = """
77
- <!DOCTYPE html>
78
- <html lang="en">
79
- <head>
80
- <meta charset="UTF-8" />
81
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
82
- <title>ImagePress — Smart Compressor</title>
83
- <link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@700;800&display=swap" rel="stylesheet"/>
84
- <style>
85
- :root {
86
- --bg: #0a0a0f;
87
- --surface: #111118;
88
- --card: #16161f;
89
- --border: #2a2a3a;
90
- --accent: #7fff6e;
91
- --accent2: #6ebaff;
92
- --text: #e8e8f0;
93
- --muted: #6b6b80;
94
- --danger: #ff6b6b;
95
- --radius: 14px;
96
- }
97
- * { box-sizing: border-box; margin: 0; padding: 0; }
98
- body {
99
- background: var(--bg);
100
- color: var(--text);
101
- font-family: 'DM Mono', monospace;
102
- min-height: 100vh;
103
- display: flex;
104
- flex-direction: column;
105
- align-items: center;
106
- padding: 40px 20px 80px;
107
- }
108
- header {
109
- text-align: center;
110
- margin-bottom: 48px;
111
- }
112
- header h1 {
113
- font-family: 'Syne', sans-serif;
114
- font-size: clamp(2.4rem, 6vw, 4rem);
115
- font-weight: 800;
116
- letter-spacing: -1px;
117
- background: linear-gradient(135deg, var(--accent) 0%, var(--accent2) 100%);
118
- -webkit-background-clip: text;
119
- -webkit-text-fill-color: transparent;
120
- }
121
- header p {
122
- color: var(--muted);
123
- font-size: 0.85rem;
124
- margin-top: 8px;
125
- letter-spacing: 0.05em;
126
- }
127
-
128
- .card {
129
- background: var(--card);
130
- border: 1px solid var(--border);
131
- border-radius: var(--radius);
132
- padding: 32px;
133
- width: 100%;
134
- max-width: 640px;
135
- }
136
-
137
- /* Drop zone */
138
- #drop-zone {
139
- border: 2px dashed var(--border);
140
- border-radius: 10px;
141
- padding: 48px 24px;
142
- text-align: center;
143
- cursor: pointer;
144
- transition: border-color .2s, background .2s;
145
- position: relative;
146
- }
147
- #drop-zone.dragover {
148
- border-color: var(--accent);
149
- background: rgba(127,255,110,.05);
150
- }
151
- #drop-zone .icon { font-size: 2.8rem; margin-bottom: 12px; }
152
- #drop-zone p { color: var(--muted); font-size: 0.8rem; line-height: 1.6; }
153
- #drop-zone strong { color: var(--text); }
154
- #file-input { display: none; }
155
-
156
- /* Preview */
157
- #preview-wrap {
158
- display: none;
159
- margin-top: 20px;
160
- border-radius: 10px;
161
- overflow: hidden;
162
- border: 1px solid var(--border);
163
- position: relative;
164
- }
165
- #preview-wrap img {
166
- width: 100%;
167
- max-height: 260px;
168
- object-fit: contain;
169
- display: block;
170
- background: #0d0d14;
171
- }
172
- #preview-meta {
173
- display: flex;
174
- justify-content: space-between;
175
- padding: 10px 14px;
176
- font-size: 0.75rem;
177
- color: var(--muted);
178
- background: var(--surface);
179
- border-top: 1px solid var(--border);
180
- }
181
-
182
- /* Controls */
183
- .controls { margin-top: 28px; }
184
- label.field-label {
185
- display: block;
186
- font-size: 0.72rem;
187
- color: var(--muted);
188
- letter-spacing: .08em;
189
- text-transform: uppercase;
190
- margin-bottom: 8px;
191
- }
192
-
193
- .target-row {
194
- display: flex;
195
- gap: 10px;
196
- align-items: center;
197
- }
198
- .target-row input[type="number"] {
199
- flex: 1;
200
- background: var(--surface);
201
- border: 1px solid var(--border);
202
- border-radius: 8px;
203
- padding: 12px 14px;
204
- color: var(--text);
205
- font-family: 'DM Mono', monospace;
206
- font-size: 1rem;
207
- outline: none;
208
- transition: border-color .2s;
209
- }
210
- .target-row input[type="number"]:focus { border-color: var(--accent); }
211
- .unit-toggle {
212
- display: flex;
213
- border: 1px solid var(--border);
214
- border-radius: 8px;
215
- overflow: hidden;
216
- }
217
- .unit-toggle button {
218
- padding: 10px 18px;
219
- background: var(--surface);
220
- border: none;
221
- color: var(--muted);
222
- font-family: 'DM Mono', monospace;
223
- font-size: 0.82rem;
224
- cursor: pointer;
225
- transition: background .15s, color .15s;
226
- }
227
- .unit-toggle button.active {
228
- background: var(--accent);
229
- color: #000;
230
- font-weight: 600;
231
- }
232
-
233
- /* Compress button */
234
- #compress-btn {
235
- margin-top: 24px;
236
- width: 100%;
237
- padding: 15px;
238
- border: none;
239
- border-radius: 10px;
240
- background: linear-gradient(135deg, var(--accent), var(--accent2));
241
- color: #000;
242
- font-family: 'Syne', sans-serif;
243
- font-size: 1rem;
244
- font-weight: 700;
245
- cursor: pointer;
246
- transition: opacity .2s, transform .1s;
247
- letter-spacing: .02em;
248
- }
249
- #compress-btn:hover { opacity: .88; }
250
- #compress-btn:active { transform: scale(.98); }
251
- #compress-btn:disabled { opacity: .35; cursor: not-allowed; }
252
-
253
- /* Spinner */
254
- #spinner {
255
- display: none;
256
- text-align: center;
257
- margin-top: 20px;
258
- color: var(--muted);
259
- font-size: 0.8rem;
260
- }
261
- .dot-loader span {
262
- display: inline-block;
263
- width: 7px; height: 7px;
264
- border-radius: 50%;
265
- background: var(--accent);
266
- margin: 0 3px;
267
- animation: bounce 1s infinite;
268
- }
269
- .dot-loader span:nth-child(2) { animation-delay: .15s; }
270
- .dot-loader span:nth-child(3) { animation-delay: .3s; }
271
- @keyframes bounce {
272
- 0%,80%,100% { transform: translateY(0); }
273
- 40% { transform: translateY(-8px); }
274
- }
275
-
276
- /* Result */
277
- #result-card {
278
- display: none;
279
- margin-top: 28px;
280
- border: 1px solid var(--border);
281
- border-radius: var(--radius);
282
- overflow: hidden;
283
- }
284
- .result-header {
285
- background: var(--surface);
286
- padding: 14px 20px;
287
- font-family: 'Syne', sans-serif;
288
- font-size: 0.85rem;
289
- font-weight: 700;
290
- color: var(--accent);
291
- letter-spacing: .05em;
292
- border-bottom: 1px solid var(--border);
293
- }
294
- .stat-grid {
295
- display: grid;
296
- grid-template-columns: 1fr 1fr 1fr;
297
- gap: 1px;
298
- background: var(--border);
299
- }
300
- .stat {
301
- background: var(--card);
302
- padding: 18px 16px;
303
- text-align: center;
304
- }
305
- .stat .val {
306
- font-family: 'Syne', sans-serif;
307
- font-size: 1.3rem;
308
- font-weight: 800;
309
- }
310
- .stat .lbl { color: var(--muted); font-size: 0.68rem; margin-top: 4px; letter-spacing: .06em; }
311
- .stat.saving .val { color: var(--accent); }
312
-
313
- #download-btn {
314
- display: block;
315
- width: calc(100% - 32px);
316
- margin: 16px;
317
- padding: 13px;
318
- border: 1px solid var(--accent);
319
- border-radius: 9px;
320
- background: transparent;
321
- color: var(--accent);
322
- font-family: 'Syne', sans-serif;
323
- font-size: 0.9rem;
324
- font-weight: 700;
325
- cursor: pointer;
326
- text-align: center;
327
- text-decoration: none;
328
- transition: background .2s, color .2s;
329
- letter-spacing: .04em;
330
- }
331
- #download-btn:hover { background: var(--accent); color: #000; }
332
-
333
- #error-msg {
334
- display: none;
335
- margin-top: 16px;
336
- padding: 12px 16px;
337
- border-radius: 8px;
338
- background: rgba(255,107,107,.1);
339
- border: 1px solid var(--danger);
340
- color: var(--danger);
341
- font-size: 0.78rem;
342
- }
343
- </style>
344
- </head>
345
- <body>
346
- <header>
347
- <h1>ImagePress</h1>
348
- <p>// compress without the blur · target any file size · zero quality loss</p>
349
- </header>
350
-
351
- <div class="card">
352
- <!-- Drop zone -->
353
- <div id="drop-zone">
354
- <div class="icon">🗜️</div>
355
- <p><strong>Drop your image here</strong><br/>or click to browse<br/><span style="font-size:.72rem">PNG · JPG · JPEG · WEBP &nbsp;|&nbsp; up to 50 MB</span></p>
356
- <input type="file" id="file-input" accept=".png,.jpg,.jpeg,.webp"/>
357
- </div>
358
-
359
- <!-- Preview -->
360
- <div id="preview-wrap">
361
- <img id="preview-img" src="" alt="preview"/>
362
- <div id="preview-meta">
363
- <span id="meta-name">—</span>
364
- <span id="meta-size">—</span>
365
- <span id="meta-dim">—</span>
366
- </div>
367
- </div>
368
-
369
- <!-- Controls -->
370
- <div class="controls">
371
- <label class="field-label">Target file size</label>
372
- <div class="target-row">
373
- <input type="number" id="target-val" min="1" placeholder="e.g. 200" value="200"/>
374
- <div class="unit-toggle">
375
- <button id="btn-kb" class="active" onclick="setUnit('KB')">KB</button>
376
- <button id="btn-mb" onclick="setUnit('MB')">MB</button>
377
- </div>
378
- </div>
379
- </div>
380
 
381
- <button id="compress-btn" disabled onclick="compress()"> Compress Image</button>
 
382
 
383
- <div id="spinner">
384
- <div class="dot-loader"><span></span><span></span><span></span></div>
385
- <p style="margin-top:10px">Compressing without touching quality…</p>
386
- </div>
387
 
388
- <div id="error-msg"></div>
389
- </div>
390
 
391
- <!-- Result -->
392
- <div class="card" style="margin-top:20px; max-width:640px; width:100%;">
393
- <div id="result-card">
394
- <div class="result-header">✅ COMPRESSION RESULT</div>
395
- <div class="stat-grid">
396
- <div class="stat">
397
- <div class="val" id="r-original">—</div>
398
- <div class="lbl">ORIGINAL</div>
399
- </div>
400
- <div class="stat">
401
- <div class="val" id="r-compressed">—</div>
402
- <div class="lbl">COMPRESSED</div>
403
- </div>
404
- <div class="stat saving">
405
- <div class="val" id="r-saving">—</div>
406
- <div class="lbl">SAVED</div>
407
- </div>
408
- </div>
409
- <a id="download-btn" href="#" download>⬇ Download Compressed Image</a>
410
- </div>
411
- </div>
412
 
413
- <script>
414
- let selectedFile = null;
415
- let currentUnit = 'KB';
416
- let downloadUrl = null;
417
 
418
- const dropZone = document.getElementById('drop-zone');
419
- const fileInput = document.getElementById('file-input');
420
 
421
- dropZone.addEventListener('click', () => fileInput.click());
422
- dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('dragover'); });
423
- dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
424
- dropZone.addEventListener('drop', e => {
425
- e.preventDefault();
426
- dropZone.classList.remove('dragover');
427
- if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
428
- });
429
- fileInput.addEventListener('change', () => { if (fileInput.files[0]) handleFile(fileInput.files[0]); });
430
 
431
- function handleFile(file) {
432
- selectedFile = file;
433
- const reader = new FileReader();
434
- reader.onload = ev => {
435
- document.getElementById('preview-img').src = ev.target.result;
436
- document.getElementById('preview-wrap').style.display = 'block';
437
- };
438
- reader.readAsDataURL(file);
439
 
440
- const img = new window.Image();
441
- img.onload = () => {
442
- document.getElementById('meta-dim').textContent = img.width + ' × ' + img.height;
443
- };
444
- img.src = URL.createObjectURL(file);
445
-
446
- document.getElementById('meta-name').textContent = file.name;
447
- document.getElementById('meta-size').textContent = formatBytes(file.size);
448
- document.getElementById('compress-btn').disabled = false;
449
- document.getElementById('result-card').style.display = 'none';
450
- document.getElementById('error-msg').style.display = 'none';
451
- if (downloadUrl) { URL.revokeObjectURL(downloadUrl); downloadUrl = null; }
452
- }
453
-
454
- function setUnit(u) {
455
- currentUnit = u;
456
- document.getElementById('btn-kb').classList.toggle('active', u === 'KB');
457
- document.getElementById('btn-mb').classList.toggle('active', u === 'MB');
458
- }
459
-
460
- function formatBytes(b) {
461
- return b < 1048576 ? (b/1024).toFixed(1)+' KB' : (b/1048576).toFixed(2)+' MB';
462
- }
463
-
464
- async function compress() {
465
- if (!selectedFile) return;
466
- const val = parseFloat(document.getElementById('target-val').value);
467
- if (!val || val <= 0) { showError('Please enter a valid target size.'); return; }
468
-
469
- const targetBytes = currentUnit === 'KB' ? val * 1024 : val * 1024 * 1024;
470
-
471
- document.getElementById('compress-btn').disabled = true;
472
- document.getElementById('spinner').style.display = 'block';
473
- document.getElementById('result-card').style.display = 'none';
474
- document.getElementById('error-msg').style.display = 'none';
475
-
476
- const fd = new FormData();
477
- fd.append('image', selectedFile);
478
- fd.append('target_bytes', Math.round(targetBytes));
479
-
480
- try {
481
- const res = await fetch('/compress', { method: 'POST', body: fd });
482
- if (!res.ok) {
483
- const j = await res.json().catch(() => ({}));
484
- throw new Error(j.error || 'Server error');
485
- }
486
- const blob = await res.blob();
487
- const origSize = selectedFile.size;
488
- const compSize = blob.size;
489
- const saved = Math.max(0, origSize - compSize);
490
- const pct = ((saved / origSize) * 100).toFixed(1);
491
-
492
- document.getElementById('r-original').textContent = formatBytes(origSize);
493
- document.getElementById('r-compressed').textContent = formatBytes(compSize);
494
- document.getElementById('r-saving').textContent = pct + '%';
495
-
496
- if (downloadUrl) URL.revokeObjectURL(downloadUrl);
497
- downloadUrl = URL.createObjectURL(blob);
498
-
499
- const ext = res.headers.get('X-Output-Format') || 'jpg';
500
- const baseName = selectedFile.name.replace(/\.[^.]+$/, '');
501
- const dlBtn = document.getElementById('download-btn');
502
- dlBtn.href = downloadUrl;
503
- dlBtn.download = baseName + '_compressed.' + ext.toLowerCase();
504
-
505
- document.getElementById('result-card').style.display = 'block';
506
- } catch(err) {
507
- showError(err.message || 'Compression failed. Please try again.');
508
- } finally {
509
- document.getElementById('compress-btn').disabled = false;
510
- document.getElementById('spinner').style.display = 'none';
511
- }
512
- }
513
-
514
- function showError(msg) {
515
- const el = document.getElementById('error-msg');
516
- el.textContent = '⚠ ' + msg;
517
- el.style.display = 'block';
518
- }
519
- </script>
520
- </body>
521
- </html>
522
- """
523
-
524
- @app.route('/')
525
- def index():
526
- return render_template_string(HTML_PAGE)
527
-
528
- @app.route('/compress', methods=['POST'])
529
- def compress():
530
- if 'image' not in request.files:
531
- return jsonify({'error': 'No image uploaded'}), 400
532
-
533
- file = request.files['image']
534
- if file.filename == '' or not allowed_file(file.filename):
535
- return jsonify({'error': 'Unsupported file type. Use PNG, JPG, JPEG, or WEBP.'}), 400
536
-
537
- try:
538
- target_bytes = int(request.form.get('target_bytes', 0))
539
- except ValueError:
540
- return jsonify({'error': 'Invalid target size'}), 400
541
-
542
- if target_bytes <= 0:
543
- return jsonify({'error': 'Target size must be greater than zero'}), 400
544
-
545
- try:
546
- img_data = file.read()
547
- original_size = len(img_data)
548
-
549
- img = Image.open(io.BytesIO(img_data))
550
-
551
- # Normalize mode
552
- fmt = img.format or 'JPEG'
553
- if img.mode in ('RGBA', 'LA', 'P'):
554
- if fmt == 'JPEG':
555
- # JPEG can't handle alpha; convert to RGB
556
- background = Image.new('RGB', img.size, (255, 255, 255))
557
- if img.mode == 'P':
558
- img = img.convert('RGBA')
559
- background.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None)
560
- img = background
561
- elif img.mode not in ('RGB', 'L'):
562
- img = img.convert('RGB')
563
-
564
- if target_bytes >= original_size:
565
- # Already within target — return optimized original
566
- buf = io.BytesIO()
567
- save_fmt = 'PNG' if fmt == 'PNG' else 'JPEG'
568
- img.save(buf, format=save_fmt, optimize=True, quality=95)
569
- buf.seek(0)
570
- out_ext = 'png' if save_fmt == 'PNG' else 'jpg'
571
- response = send_file(buf, mimetype=f'image/{out_ext}',
572
- as_attachment=False,
573
- download_name=f'compressed.{out_ext}')
574
- response.headers['X-Output-Format'] = out_ext
575
- return response
576
 
577
- buf, final_size, out_fmt = compress_to_target(img, fmt, target_bytes, original_size)
578
- out_ext = 'webp' if out_fmt == 'WEBP' else ('png' if out_fmt == 'PNG' else 'jpg')
579
- mime = f'image/{out_ext}'
580
 
581
- response = send_file(buf, mimetype=mime, as_attachment=False,
582
- download_name=f'compressed.{out_ext}')
583
- response.headers['X-Output-Format'] = out_ext
584
- return response
585
 
586
- except Exception as e:
587
- return jsonify({'error': f'Processing failed: {str(e)}'}), 500
 
 
 
588
 
589
- if __name__ == '__main__':
590
- app.run(debug=True, host='0.0.0.0', port=5000)
 
 
1
  import io
 
 
2
  from PIL import Image
3
+ import gradio as gr
4
 
5
+ def compress_image(file, target_value, unit):
6
+ if file is None:
7
+ return None, "Please upload an image"
8
 
9
+ img = Image.open(file)
10
+ original_bytes_io = io.BytesIO()
11
+ img.save(original_bytes_io, format="PNG")
12
+ original_size = original_bytes_io.tell()
13
 
14
+ target_bytes = target_value * 1024 if unit == "KB" else target_value * 1024 * 1024
 
15
 
16
+ def compress(img, target_bytes):
17
+ for q in range(95, 74, -2):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  buf = io.BytesIO()
19
+ img.convert("RGB").save(buf, format="JPEG", quality=q, optimize=True)
20
  if buf.tell() <= target_bytes:
21
  buf.seek(0)
22
+ return buf
23
  buf.seek(0)
24
+ return buf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
+ if target_bytes >= original_size:
27
+ return img, f"Already under target size ({original_size/1024:.1f} KB)"
28
 
29
+ compressed_buf = compress(img, target_bytes)
30
+ compressed_img = Image.open(compressed_buf)
 
 
31
 
32
+ saved = original_size - compressed_buf.getbuffer().nbytes
33
+ pct = (saved / original_size) * 100
34
 
35
+ msg = f"Original: {original_size/1024:.1f} KB | Compressed: {compressed_buf.getbuffer().nbytes/1024:.1f} KB | Saved: {pct:.1f}%"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ return compressed_img, msg
 
 
 
38
 
 
 
39
 
40
+ with gr.Blocks() as demo:
41
+ gr.Markdown("# 🗜️ ImagePress (Hugging Face Version)")
 
 
 
 
 
 
 
42
 
43
+ with gr.Row():
44
+ input_img = gr.Image(type="filepath", label="Upload Image")
45
+ output_img = gr.Image(label="Compressed Image")
 
 
 
 
 
46
 
47
+ target = gr.Number(value=200, label="Target Size")
48
+ unit = gr.Radio(["KB", "MB"], value="KB", label="Unit")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
+ btn = gr.Button("⚡ Compress")
 
 
51
 
52
+ result = gr.Textbox()
 
 
 
53
 
54
+ btn.click(
55
+ compress_image,
56
+ inputs=[input_img, target, unit],
57
+ outputs=[output_img, result]
58
+ )
59
 
60
+ demo.launch()