bharathkumarms commited on
Commit
0f5fe41
·
verified ·
1 Parent(s): 225070f

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +653 -656
main.py CHANGED
@@ -1,657 +1,654 @@
1
- from fastapi import FastAPI, File, UploadFile, HTTPException
2
- from fastapi.responses import HTMLResponse
3
- from PIL import Image
4
- from PIL.ExifTags import TAGS
5
- from datetime import datetime
6
- import os
7
-
8
- app = FastAPI(title="Image Metadata Extractor", description="Upload an image to extract its metadata including creation timestamp")
9
-
10
- # Create uploads directory if it doesn't exist
11
- UPLOAD_DIR = "uploads"
12
- if not os.path.exists(UPLOAD_DIR):
13
- os.makedirs(UPLOAD_DIR)
14
-
15
- def get_image_metadata(image_path):
16
- """Extract metadata from an image file"""
17
- result = {
18
- "filename": os.path.basename(image_path),
19
- "format": None,
20
- "size": None,
21
- "mode": None,
22
- "exif_data": {},
23
- "creation_date": None,
24
- "file_system_creation_time": None,
25
- "error": None
26
- }
27
-
28
- try:
29
- # Open the image
30
- with Image.open(image_path) as img:
31
- result["format"] = img.format
32
- result["size"] = img.size
33
- result["mode"] = img.mode
34
-
35
- # Get EXIF data
36
- exif_data = img._getexif()
37
- if exif_data is not None:
38
- exif_dict = {}
39
- for tag_id, value in exif_data.items():
40
- tag = TAGS.get(tag_id, tag_id)
41
- exif_dict[tag] = str(value)
42
- result["exif_data"] = exif_dict
43
-
44
- # Try to extract creation date from common EXIF tags
45
- date_tags = ['DateTime', 'DateTimeOriginal', 'DateTimeDigitized']
46
- for tag in date_tags:
47
- # Find the tag ID for the current tag
48
- tag_id = None
49
- for tid, tname in TAGS.items():
50
- if tname == tag:
51
- tag_id = tid
52
- break
53
-
54
- if tag_id and tag_id in exif_data:
55
- raw_date = exif_data[tag_id]
56
- try:
57
- # Handle different date formats
58
- if isinstance(raw_date, bytes):
59
- raw_date = raw_date.decode('utf-8')
60
-
61
- # Try different date formats
62
- formats_to_try = [
63
- '%Y:%m:%d %H:%M:%S',
64
- '%Y-%m-%d %H:%M:%S',
65
- '%Y/%m/%d %H:%M:%S'
66
- ]
67
-
68
- creation_date = None
69
- for fmt in formats_to_try:
70
- try:
71
- creation_date = datetime.strptime(str(raw_date), fmt)
72
- break
73
- except ValueError:
74
- continue
75
-
76
- if creation_date:
77
- result["creation_date"] = {
78
- "source": tag,
79
- "date": creation_date.isoformat()
80
- }
81
- break
82
- except Exception:
83
- continue
84
-
85
- # Fallback: Use file system creation time
86
- stat = os.stat(image_path)
87
- creation_time = stat.st_ctime
88
- result["file_system_creation_time"] = datetime.fromtimestamp(creation_time).isoformat()
89
-
90
- except Exception as e:
91
- result["error"] = str(e)
92
-
93
- return result
94
-
95
- @app.get("/", response_class=HTMLResponse)
96
- async def read_root():
97
- """Serve the HTML frontend"""
98
- return """
99
- <!DOCTYPE html>
100
- <html>
101
- <head>
102
- <title>Image Metadata Extractor</title>
103
- <style>
104
- :root {
105
- --primary-color: #4361ee;
106
- --secondary-color: #3f37c9;
107
- --accent-color: #4895ef;
108
- --success-color: #4cc9f0;
109
- --light-color: #f8f9fa;
110
- --dark-color: #212529;
111
- --danger-color: #f72585;
112
- --warning-color: #f8961e;
113
- --info-color: #56cfe1;
114
- }
115
-
116
- * {
117
- margin: 0;
118
- padding: 0;
119
- box-sizing: border-box;
120
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
121
- }
122
-
123
- body {
124
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
125
- min-height: 100vh;
126
- padding: 20px;
127
- }
128
-
129
- .container {
130
- max-width: 1000px;
131
- margin: 0 auto;
132
- }
133
-
134
- header {
135
- text-align: center;
136
- padding: 30px 0;
137
- color: white;
138
- text-shadow: 0 2px 4px rgba(0,0,0,0.3);
139
- }
140
-
141
- h1 {
142
- font-size: 2.5rem;
143
- margin-bottom: 10px;
144
- }
145
-
146
- .subtitle {
147
- font-size: 1.2rem;
148
- opacity: 0.9;
149
- }
150
-
151
- .card {
152
- background: rgba(255, 255, 255, 0.95);
153
- border-radius: 15px;
154
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
155
- padding: 30px;
156
- margin-bottom: 30px;
157
- backdrop-filter: blur(10px);
158
- }
159
-
160
- .upload-area {
161
- border: 3px dashed var(--accent-color);
162
- border-radius: 12px;
163
- padding: 40px 20px;
164
- text-align: center;
165
- transition: all 0.3s ease;
166
- background: rgba(72, 149, 239, 0.05);
167
- cursor: pointer;
168
- }
169
-
170
- .upload-area:hover {
171
- border-color: var(--primary-color);
172
- background: rgba(67, 97, 238, 0.1);
173
- transform: translateY(-2px);
174
- }
175
-
176
- .upload-area.active {
177
- border-color: var(--success-color);
178
- background: rgba(76, 201, 240, 0.1);
179
- }
180
-
181
- .upload-icon {
182
- font-size: 4rem;
183
- color: var(--accent-color);
184
- margin-bottom: 20px;
185
- }
186
-
187
- .upload-text {
188
- font-size: 1.3rem;
189
- color: var(--dark-color);
190
- margin-bottom: 15px;
191
- }
192
-
193
- .upload-hint {
194
- color: #6c757d;
195
- margin-bottom: 20px;
196
- }
197
-
198
- .file-input {
199
- display: none;
200
- }
201
-
202
- .upload-btn {
203
- background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
204
- color: white;
205
- border: none;
206
- padding: 15px 40px;
207
- font-size: 1.1rem;
208
- border-radius: 50px;
209
- cursor: pointer;
210
- transition: all 0.3s ease;
211
- box-shadow: 0 4px 15px rgba(67, 97, 238, 0.3);
212
- }
213
-
214
- .upload-btn:hover {
215
- transform: translateY(-2px);
216
- box-shadow: 0 6px 20px rgba(67, 97, 238, 0.4);
217
- }
218
-
219
- .upload-btn:active {
220
- transform: translateY(0);
221
- }
222
-
223
- .result-container {
224
- display: none;
225
- }
226
-
227
- .result-header {
228
- text-align: center;
229
- margin-bottom: 25px;
230
- color: var(--dark-color);
231
- }
232
-
233
- .metadata-grid {
234
- display: grid;
235
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
236
- gap: 20px;
237
- margin-bottom: 30px;
238
- }
239
-
240
- .metadata-card {
241
- background: white;
242
- border-radius: 10px;
243
- padding: 20px;
244
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
245
- border-left: 4px solid var(--primary-color);
246
- transition: transform 0.3s ease;
247
- }
248
-
249
- .metadata-card:hover {
250
- transform: translateY(-5px);
251
- box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
252
- }
253
-
254
- .metadata-card.creation {
255
- border-left-color: var(--success-color);
256
- }
257
-
258
- .metadata-card.exif {
259
- border-left-color: var(--warning-color);
260
- }
261
-
262
- .metadata-card h3 {
263
- color: var(--primary-color);
264
- margin-bottom: 15px;
265
- font-size: 1.3rem;
266
- }
267
-
268
- .metadata-card.creation h3 {
269
- color: var(--success-color);
270
- }
271
-
272
- .metadata-card.exif h3 {
273
- color: var(--warning-color);
274
- }
275
-
276
- .metadata-item {
277
- margin: 12px 0;
278
- padding: 10px;
279
- background: #f8f9fa;
280
- border-radius: 8px;
281
- border-left: 3px solid var(--accent-color);
282
- }
283
-
284
- .metadata-item strong {
285
- display: block;
286
- color: var(--dark-color);
287
- margin-bottom: 5px;
288
- }
289
-
290
- .metadata-item span {
291
- color: #6c757d;
292
- }
293
-
294
- .exif-list {
295
- max-height: 300px;
296
- overflow-y: auto;
297
- }
298
-
299
- .exif-item {
300
- display: flex;
301
- justify-content: space-between;
302
- padding: 8px 0;
303
- border-bottom: 1px solid #eee;
304
- }
305
-
306
- .exif-item:last-child {
307
- border-bottom: none;
308
- }
309
-
310
- .exif-key {
311
- font-weight: 500;
312
- color: var(--dark-color);
313
- }
314
-
315
- .exif-value {
316
- color: #6c757d;
317
- text-align: right;
318
- max-width: 60%;
319
- word-break: break-word;
320
- }
321
-
322
- .error {
323
- background: #ffebee;
324
- border-left-color: var(--danger-color);
325
- color: var(--danger-color);
326
- }
327
-
328
- .error strong {
329
- color: var(--danger-color);
330
- }
331
-
332
- .actions {
333
- text-align: center;
334
- margin-top: 20px;
335
- }
336
-
337
- .reset-btn {
338
- background: transparent;
339
- color: var(--primary-color);
340
- border: 2px solid var(--primary-color);
341
- padding: 12px 30px;
342
- font-size: 1rem;
343
- border-radius: 50px;
344
- cursor: pointer;
345
- transition: all 0.3s ease;
346
- }
347
-
348
- .reset-btn:hover {
349
- background: var(--primary-color);
350
- color: white;
351
- }
352
-
353
- footer {
354
- text-align: center;
355
- color: rgba(255, 255, 255, 0.8);
356
- padding: 20px 0;
357
- font-size: 0.9rem;
358
- }
359
-
360
- @media (max-width: 768px) {
361
- .metadata-grid {
362
- grid-template-columns: 1fr;
363
- }
364
-
365
- h1 {
366
- font-size: 2rem;
367
- }
368
-
369
- .card {
370
- padding: 20px;
371
- }
372
- }
373
- </style>
374
- </head>
375
- <body>
376
- <div class="container">
377
- <header>
378
- <h1>📸 Image Metadata Extractor</h1>
379
- <p class="subtitle">Upload any image to extract detailed metadata including creation timestamps</p>
380
- </header>
381
-
382
- <main>
383
- <div class="card">
384
- <div class="upload-area" id="upload-area">
385
- <div class="upload-icon">📁</div>
386
- <h2 class="upload-text">Drag & Drop your image here</h2>
387
- <p class="upload-hint">or click to browse files</p>
388
- <input type="file" id="file-input" class="file-input" accept="image/*">
389
- <button class="upload-btn" id="upload-btn">Select Image</button>
390
- </div>
391
- </div>
392
-
393
- <div class="card result-container" id="result-container">
394
- <h2 class="result-header">Image Metadata Analysis</h2>
395
- <div class="metadata-grid" id="metadata-content"></div>
396
- <div class="actions">
397
- <button class="reset-btn" id="reset-btn">Analyze Another Image</button>
398
- </div>
399
- </div>
400
- </main>
401
-
402
- <footer>
403
- <p>Image Metadata Extractor &copy; 2025 | Extract creation timestamps and EXIF data</p>
404
- </footer>
405
- </div>
406
-
407
- <script>
408
- const uploadArea = document.getElementById('upload-area');
409
- const fileInput = document.getElementById('file-input');
410
- const uploadBtn = document.getElementById('upload-btn');
411
- const resultContainer = document.getElementById('result-container');
412
- const metadataContent = document.getElementById('metadata-content');
413
- const resetBtn = document.getElementById('reset-btn');
414
-
415
- // Event listeners
416
- uploadBtn.addEventListener('click', () => fileInput.click());
417
- fileInput.addEventListener('change', handleFileSelect);
418
- uploadArea.addEventListener('dragover', handleDragOver);
419
- uploadArea.addEventListener('dragleave', handleDragLeave);
420
- uploadArea.addEventListener('drop', handleDrop);
421
- resetBtn.addEventListener('click', resetForm);
422
-
423
- function handleDragOver(e) {
424
- e.preventDefault();
425
- uploadArea.classList.add('active');
426
- }
427
-
428
- function handleDragLeave() {
429
- uploadArea.classList.remove('active');
430
- }
431
-
432
- function handleDrop(e) {
433
- e.preventDefault();
434
- uploadArea.classList.remove('active');
435
-
436
- if (e.dataTransfer.files.length) {
437
- fileInput.files = e.dataTransfer.files;
438
- processFile(fileInput.files[0]);
439
- }
440
- }
441
-
442
- function handleFileSelect() {
443
- if (fileInput.files.length) {
444
- processFile(fileInput.files[0]);
445
- }
446
- }
447
-
448
- async function processFile(file) {
449
- if (!file.type.startsWith('image/')) {
450
- alert('Please select an image file');
451
- return;
452
- }
453
-
454
- const formData = new FormData();
455
- formData.append('file', file);
456
-
457
- try {
458
- // Show loading state
459
- uploadBtn.textContent = 'Processing...';
460
- uploadBtn.disabled = true;
461
-
462
- const response = await fetch('/extract-metadata', {
463
- method: 'POST',
464
- body: formData
465
- });
466
-
467
- const data = await response.json();
468
-
469
- if (response.ok) {
470
- displayMetadata(data);
471
- } else {
472
- showError(data.detail || 'Error processing image');
473
- }
474
- } catch (error) {
475
- showError('Error: ' + error.message);
476
- } finally {
477
- uploadBtn.textContent = 'Select Image';
478
- uploadBtn.disabled = false;
479
- }
480
- }
481
-
482
- function displayMetadata(data) {
483
- // Hide upload area and show results
484
- uploadArea.style.display = 'none';
485
- resultContainer.style.display = 'block';
486
-
487
- // Generate metadata cards
488
- let html = '';
489
-
490
- // Basic info card
491
- html += `
492
- <div class="metadata-card">
493
- <h3>📋 Basic Information</h3>
494
- <div class="metadata-item">
495
- <strong>Filename</strong>
496
- <span>${data.filename}</span>
497
- </div>
498
- <div class="metadata-item">
499
- <strong>Format</strong>
500
- <span>${data.format || 'Unknown'}</span>
501
- </div>
502
- <div class="metadata-item">
503
- <strong>Dimensions</strong>
504
- <span>${data.size ? `${data.size[0]} × ${data.size[1]} pixels` : 'Unknown'}</span>
505
- </div>
506
- <div class="metadata-item">
507
- <strong>Color Mode</strong>
508
- <span>${data.mode || 'Unknown'}</span>
509
- </div>
510
- </div>
511
- `;
512
-
513
- // Creation date card
514
- html += `
515
- <div class="metadata-card creation">
516
- <h3>⏱️ Creation Timestamp</h3>
517
- `;
518
-
519
- if (data.creation_date) {
520
- const date = new Date(data.creation_date.date);
521
- html += `
522
- <div class="metadata-item">
523
- <strong>Source</strong>
524
- <span>${data.creation_date.source}</span>
525
- </div>
526
- <div class="metadata-item">
527
- <strong>Date & Time</strong>
528
- <span>${date.toLocaleString()}</span>
529
- </div>
530
- `;
531
- } else if (data.file_system_creation_time) {
532
- const date = new Date(data.file_system_creation_time);
533
- html += `
534
- <div class="metadata-item">
535
- <strong>Source</strong>
536
- <span>File System</span>
537
- </div>
538
- <div class="metadata-item">
539
- <strong>Date & Time</strong>
540
- <span>${date.toLocaleString()}</span>
541
- </div>
542
- `;
543
- } else {
544
- html += `
545
- <div class="metadata-item error">
546
- <strong>No Creation Date Found</strong>
547
- <span>Neither EXIF data nor file system timestamps contain creation information</span>
548
- </div>
549
- `;
550
- }
551
-
552
- html += `</div>`;
553
-
554
- // EXIF data card
555
- html += `
556
- <div class="metadata-card exif">
557
- <h3>🔍 EXIF Metadata</h3>
558
- `;
559
-
560
- if (Object.keys(data.exif_data).length > 0) {
561
- html += `<div class="exif-list">`;
562
- for (const [key, value] of Object.entries(data.exif_data)) {
563
- html += `
564
- <div class="exif-item">
565
- <span class="exif-key">${key}</span>
566
- <span class="exif-value">${value}</span>
567
- </div>
568
- `;
569
- }
570
- html += `</div>`;
571
- } else {
572
- html += `
573
- <div class="metadata-item">
574
- <strong>No EXIF Data</strong>
575
- <span>This image doesn't contain EXIF metadata</span>
576
- </div>
577
- `;
578
- }
579
-
580
- html += `</div>`;
581
-
582
- // Error card (if any)
583
- if (data.error) {
584
- html += `
585
- <div class="metadata-card">
586
- <h3>⚠️ Error</h3>
587
- <div class="metadata-item error">
588
- <strong>Processing Error</strong>
589
- <span>${data.error}</span>
590
- </div>
591
- </div>
592
- `;
593
- }
594
-
595
- metadataContent.innerHTML = html;
596
- }
597
-
598
- function showError(message) {
599
- // Hide upload area and show results
600
- uploadArea.style.display = 'none';
601
- resultContainer.style.display = 'block';
602
-
603
- metadataContent.innerHTML = `
604
- <div class="metadata-card">
605
- <h3>⚠️ Error</h3>
606
- <div class="metadata-item error">
607
- <strong>Processing Failed</strong>
608
- <span>${message}</span>
609
- </div>
610
- </div>
611
- `;
612
- }
613
-
614
- function resetForm() {
615
- // Show upload area and hide results
616
- uploadArea.style.display = 'block';
617
- resultContainer.style.display = 'none';
618
-
619
- // Reset file input
620
- fileInput.value = '';
621
- }
622
- </script>
623
- </body>
624
- </html>
625
- """
626
-
627
- @app.post("/extract-metadata")
628
- async def extract_metadata(file: UploadFile = File(...)):
629
- """Endpoint to extract metadata from uploaded image"""
630
- if not file.content_type.startswith("image/"):
631
- raise HTTPException(status_code=400, detail="File must be an image")
632
-
633
- # Save the uploaded file temporarily
634
- file_path = os.path.join(UPLOAD_DIR, file.filename)
635
-
636
- try:
637
- # Read the file content
638
- contents = await file.read()
639
-
640
- # Save to temporary file
641
- with open(file_path, "wb") as f:
642
- f.write(contents)
643
-
644
- # Extract metadata
645
- metadata = get_image_metadata(file_path)
646
-
647
- return metadata
648
- except Exception as e:
649
- raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)}")
650
- finally:
651
- # Clean up temporary file
652
- if os.path.exists(file_path):
653
- os.remove(file_path)
654
-
655
- if __name__ == "__main__":
656
- import uvicorn
657
  uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException
2
+ from fastapi.responses import HTMLResponse
3
+ from PIL import Image
4
+ from PIL.ExifTags import TAGS
5
+ from datetime import datetime
6
+ import os
7
+ import tempfile
8
+
9
+ app = FastAPI(title="Image Metadata Extractor", description="Upload an image to extract its metadata including creation timestamp")
10
+
11
+ def get_image_metadata(image_path):
12
+ """Extract metadata from an image file"""
13
+ result = {
14
+ "filename": os.path.basename(image_path),
15
+ "format": None,
16
+ "size": None,
17
+ "mode": None,
18
+ "exif_data": {},
19
+ "creation_date": None,
20
+ "file_system_creation_time": None,
21
+ "error": None
22
+ }
23
+
24
+ try:
25
+ # Open the image
26
+ with Image.open(image_path) as img:
27
+ result["format"] = img.format
28
+ result["size"] = img.size
29
+ result["mode"] = img.mode
30
+
31
+ # Get EXIF data
32
+ exif_data = img._getexif()
33
+ if exif_data is not None:
34
+ exif_dict = {}
35
+ for tag_id, value in exif_data.items():
36
+ tag = TAGS.get(tag_id, tag_id)
37
+ exif_dict[tag] = str(value)
38
+ result["exif_data"] = exif_dict
39
+
40
+ # Try to extract creation date from common EXIF tags
41
+ date_tags = ['DateTime', 'DateTimeOriginal', 'DateTimeDigitized']
42
+ for tag in date_tags:
43
+ # Find the tag ID for the current tag
44
+ tag_id = None
45
+ for tid, tname in TAGS.items():
46
+ if tname == tag:
47
+ tag_id = tid
48
+ break
49
+
50
+ if tag_id and tag_id in exif_data:
51
+ raw_date = exif_data[tag_id]
52
+ try:
53
+ # Handle different date formats
54
+ if isinstance(raw_date, bytes):
55
+ raw_date = raw_date.decode('utf-8')
56
+
57
+ # Try different date formats
58
+ formats_to_try = [
59
+ '%Y:%m:%d %H:%M:%S',
60
+ '%Y-%m-%d %H:%M:%S',
61
+ '%Y/%m/%d %H:%M:%S'
62
+ ]
63
+
64
+ creation_date = None
65
+ for fmt in formats_to_try:
66
+ try:
67
+ creation_date = datetime.strptime(str(raw_date), fmt)
68
+ break
69
+ except ValueError:
70
+ continue
71
+
72
+ if creation_date:
73
+ result["creation_date"] = {
74
+ "source": tag,
75
+ "date": creation_date.isoformat()
76
+ }
77
+ break
78
+ except Exception:
79
+ continue
80
+
81
+ # Fallback: Use file system creation time
82
+ stat = os.stat(image_path)
83
+ creation_time = stat.st_ctime
84
+ result["file_system_creation_time"] = datetime.fromtimestamp(creation_time).isoformat()
85
+
86
+ except Exception as e:
87
+ result["error"] = str(e)
88
+
89
+ return result
90
+
91
+ @app.get("/", response_class=HTMLResponse)
92
+ async def read_root():
93
+ """Serve the HTML frontend"""
94
+ return """
95
+ <!DOCTYPE html>
96
+ <html>
97
+ <head>
98
+ <title>Image Metadata Extractor</title>
99
+ <style>
100
+ :root {
101
+ --primary-color: #4361ee;
102
+ --secondary-color: #3f37c9;
103
+ --accent-color: #4895ef;
104
+ --success-color: #4cc9f0;
105
+ --light-color: #f8f9fa;
106
+ --dark-color: #212529;
107
+ --danger-color: #f72585;
108
+ --warning-color: #f8961e;
109
+ --info-color: #56cfe1;
110
+ }
111
+
112
+ * {
113
+ margin: 0;
114
+ padding: 0;
115
+ box-sizing: border-box;
116
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
117
+ }
118
+
119
+ body {
120
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
121
+ min-height: 100vh;
122
+ padding: 20px;
123
+ }
124
+
125
+ .container {
126
+ max-width: 1000px;
127
+ margin: 0 auto;
128
+ }
129
+
130
+ header {
131
+ text-align: center;
132
+ padding: 30px 0;
133
+ color: white;
134
+ text-shadow: 0 2px 4px rgba(0,0,0,0.3);
135
+ }
136
+
137
+ h1 {
138
+ font-size: 2.5rem;
139
+ margin-bottom: 10px;
140
+ }
141
+
142
+ .subtitle {
143
+ font-size: 1.2rem;
144
+ opacity: 0.9;
145
+ }
146
+
147
+ .card {
148
+ background: rgba(255, 255, 255, 0.95);
149
+ border-radius: 15px;
150
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
151
+ padding: 30px;
152
+ margin-bottom: 30px;
153
+ backdrop-filter: blur(10px);
154
+ }
155
+
156
+ .upload-area {
157
+ border: 3px dashed var(--accent-color);
158
+ border-radius: 12px;
159
+ padding: 40px 20px;
160
+ text-align: center;
161
+ transition: all 0.3s ease;
162
+ background: rgba(72, 149, 239, 0.05);
163
+ cursor: pointer;
164
+ }
165
+
166
+ .upload-area:hover {
167
+ border-color: var(--primary-color);
168
+ background: rgba(67, 97, 238, 0.1);
169
+ transform: translateY(-2px);
170
+ }
171
+
172
+ .upload-area.active {
173
+ border-color: var(--success-color);
174
+ background: rgba(76, 201, 240, 0.1);
175
+ }
176
+
177
+ .upload-icon {
178
+ font-size: 4rem;
179
+ color: var(--accent-color);
180
+ margin-bottom: 20px;
181
+ }
182
+
183
+ .upload-text {
184
+ font-size: 1.3rem;
185
+ color: var(--dark-color);
186
+ margin-bottom: 15px;
187
+ }
188
+
189
+ .upload-hint {
190
+ color: #6c757d;
191
+ margin-bottom: 20px;
192
+ }
193
+
194
+ .file-input {
195
+ display: none;
196
+ }
197
+
198
+ .upload-btn {
199
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
200
+ color: white;
201
+ border: none;
202
+ padding: 15px 40px;
203
+ font-size: 1.1rem;
204
+ border-radius: 50px;
205
+ cursor: pointer;
206
+ transition: all 0.3s ease;
207
+ box-shadow: 0 4px 15px rgba(67, 97, 238, 0.3);
208
+ }
209
+
210
+ .upload-btn:hover {
211
+ transform: translateY(-2px);
212
+ box-shadow: 0 6px 20px rgba(67, 97, 238, 0.4);
213
+ }
214
+
215
+ .upload-btn:active {
216
+ transform: translateY(0);
217
+ }
218
+
219
+ .result-container {
220
+ display: none;
221
+ }
222
+
223
+ .result-header {
224
+ text-align: center;
225
+ margin-bottom: 25px;
226
+ color: var(--dark-color);
227
+ }
228
+
229
+ .metadata-grid {
230
+ display: grid;
231
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
232
+ gap: 20px;
233
+ margin-bottom: 30px;
234
+ }
235
+
236
+ .metadata-card {
237
+ background: white;
238
+ border-radius: 10px;
239
+ padding: 20px;
240
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
241
+ border-left: 4px solid var(--primary-color);
242
+ transition: transform 0.3s ease;
243
+ }
244
+
245
+ .metadata-card:hover {
246
+ transform: translateY(-5px);
247
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
248
+ }
249
+
250
+ .metadata-card.creation {
251
+ border-left-color: var(--success-color);
252
+ }
253
+
254
+ .metadata-card.exif {
255
+ border-left-color: var(--warning-color);
256
+ }
257
+
258
+ .metadata-card h3 {
259
+ color: var(--primary-color);
260
+ margin-bottom: 15px;
261
+ font-size: 1.3rem;
262
+ }
263
+
264
+ .metadata-card.creation h3 {
265
+ color: var(--success-color);
266
+ }
267
+
268
+ .metadata-card.exif h3 {
269
+ color: var(--warning-color);
270
+ }
271
+
272
+ .metadata-item {
273
+ margin: 12px 0;
274
+ padding: 10px;
275
+ background: #f8f9fa;
276
+ border-radius: 8px;
277
+ border-left: 3px solid var(--accent-color);
278
+ }
279
+
280
+ .metadata-item strong {
281
+ display: block;
282
+ color: var(--dark-color);
283
+ margin-bottom: 5px;
284
+ }
285
+
286
+ .metadata-item span {
287
+ color: #6c757d;
288
+ }
289
+
290
+ .exif-list {
291
+ max-height: 300px;
292
+ overflow-y: auto;
293
+ }
294
+
295
+ .exif-item {
296
+ display: flex;
297
+ justify-content: space-between;
298
+ padding: 8px 0;
299
+ border-bottom: 1px solid #eee;
300
+ }
301
+
302
+ .exif-item:last-child {
303
+ border-bottom: none;
304
+ }
305
+
306
+ .exif-key {
307
+ font-weight: 500;
308
+ color: var(--dark-color);
309
+ }
310
+
311
+ .exif-value {
312
+ color: #6c757d;
313
+ text-align: right;
314
+ max-width: 60%;
315
+ word-break: break-word;
316
+ }
317
+
318
+ .error {
319
+ background: #ffebee;
320
+ border-left-color: var(--danger-color);
321
+ color: var(--danger-color);
322
+ }
323
+
324
+ .error strong {
325
+ color: var(--danger-color);
326
+ }
327
+
328
+ .actions {
329
+ text-align: center;
330
+ margin-top: 20px;
331
+ }
332
+
333
+ .reset-btn {
334
+ background: transparent;
335
+ color: var(--primary-color);
336
+ border: 2px solid var(--primary-color);
337
+ padding: 12px 30px;
338
+ font-size: 1rem;
339
+ border-radius: 50px;
340
+ cursor: pointer;
341
+ transition: all 0.3s ease;
342
+ }
343
+
344
+ .reset-btn:hover {
345
+ background: var(--primary-color);
346
+ color: white;
347
+ }
348
+
349
+ footer {
350
+ text-align: center;
351
+ color: rgba(255, 255, 255, 0.8);
352
+ padding: 20px 0;
353
+ font-size: 0.9rem;
354
+ }
355
+
356
+ @media (max-width: 768px) {
357
+ .metadata-grid {
358
+ grid-template-columns: 1fr;
359
+ }
360
+
361
+ h1 {
362
+ font-size: 2rem;
363
+ }
364
+
365
+ .card {
366
+ padding: 20px;
367
+ }
368
+ }
369
+ </style>
370
+ </head>
371
+ <body>
372
+ <div class="container">
373
+ <header>
374
+ <h1>📸 Image Metadata Extractor</h1>
375
+ <p class="subtitle">Upload any image to extract detailed metadata including creation timestamps</p>
376
+ </header>
377
+
378
+ <main>
379
+ <div class="card">
380
+ <div class="upload-area" id="upload-area">
381
+ <div class="upload-icon">📁</div>
382
+ <h2 class="upload-text">Drag & Drop your image here</h2>
383
+ <p class="upload-hint">or click to browse files</p>
384
+ <input type="file" id="file-input" class="file-input" accept="image/*">
385
+ <button class="upload-btn" id="upload-btn">Select Image</button>
386
+ </div>
387
+ </div>
388
+
389
+ <div class="card result-container" id="result-container">
390
+ <h2 class="result-header">Image Metadata Analysis</h2>
391
+ <div class="metadata-grid" id="metadata-content"></div>
392
+ <div class="actions">
393
+ <button class="reset-btn" id="reset-btn">Analyze Another Image</button>
394
+ </div>
395
+ </div>
396
+ </main>
397
+
398
+ <footer>
399
+ <p>Image Metadata Extractor &copy; 2025 | Extract creation timestamps and EXIF data</p>
400
+ </footer>
401
+ </div>
402
+
403
+ <script>
404
+ const uploadArea = document.getElementById('upload-area');
405
+ const fileInput = document.getElementById('file-input');
406
+ const uploadBtn = document.getElementById('upload-btn');
407
+ const resultContainer = document.getElementById('result-container');
408
+ const metadataContent = document.getElementById('metadata-content');
409
+ const resetBtn = document.getElementById('reset-btn');
410
+
411
+ // Event listeners
412
+ uploadBtn.addEventListener('click', () => fileInput.click());
413
+ fileInput.addEventListener('change', handleFileSelect);
414
+ uploadArea.addEventListener('dragover', handleDragOver);
415
+ uploadArea.addEventListener('dragleave', handleDragLeave);
416
+ uploadArea.addEventListener('drop', handleDrop);
417
+ resetBtn.addEventListener('click', resetForm);
418
+
419
+ function handleDragOver(e) {
420
+ e.preventDefault();
421
+ uploadArea.classList.add('active');
422
+ }
423
+
424
+ function handleDragLeave() {
425
+ uploadArea.classList.remove('active');
426
+ }
427
+
428
+ function handleDrop(e) {
429
+ e.preventDefault();
430
+ uploadArea.classList.remove('active');
431
+
432
+ if (e.dataTransfer.files.length) {
433
+ fileInput.files = e.dataTransfer.files;
434
+ processFile(fileInput.files[0]);
435
+ }
436
+ }
437
+
438
+ function handleFileSelect() {
439
+ if (fileInput.files.length) {
440
+ processFile(fileInput.files[0]);
441
+ }
442
+ }
443
+
444
+ async function processFile(file) {
445
+ if (!file.type.startsWith('image/')) {
446
+ alert('Please select an image file');
447
+ return;
448
+ }
449
+
450
+ const formData = new FormData();
451
+ formData.append('file', file);
452
+
453
+ try {
454
+ // Show loading state
455
+ uploadBtn.textContent = 'Processing...';
456
+ uploadBtn.disabled = true;
457
+
458
+ const response = await fetch('/extract-metadata', {
459
+ method: 'POST',
460
+ body: formData
461
+ });
462
+
463
+ const data = await response.json();
464
+
465
+ if (response.ok) {
466
+ displayMetadata(data);
467
+ } else {
468
+ showError(data.detail || 'Error processing image');
469
+ }
470
+ } catch (error) {
471
+ showError('Error: ' + error.message);
472
+ } finally {
473
+ uploadBtn.textContent = 'Select Image';
474
+ uploadBtn.disabled = false;
475
+ }
476
+ }
477
+
478
+ function displayMetadata(data) {
479
+ // Hide upload area and show results
480
+ uploadArea.style.display = 'none';
481
+ resultContainer.style.display = 'block';
482
+
483
+ // Generate metadata cards
484
+ let html = '';
485
+
486
+ // Basic info card
487
+ html += `
488
+ <div class="metadata-card">
489
+ <h3>📋 Basic Information</h3>
490
+ <div class="metadata-item">
491
+ <strong>Filename</strong>
492
+ <span>${data.filename}</span>
493
+ </div>
494
+ <div class="metadata-item">
495
+ <strong>Format</strong>
496
+ <span>${data.format || 'Unknown'}</span>
497
+ </div>
498
+ <div class="metadata-item">
499
+ <strong>Dimensions</strong>
500
+ <span>${data.size ? `${data.size[0]} × ${data.size[1]} pixels` : 'Unknown'}</span>
501
+ </div>
502
+ <div class="metadata-item">
503
+ <strong>Color Mode</strong>
504
+ <span>${data.mode || 'Unknown'}</span>
505
+ </div>
506
+ </div>
507
+ `;
508
+
509
+ // Creation date card
510
+ html += `
511
+ <div class="metadata-card creation">
512
+ <h3>⏱️ Creation Timestamp</h3>
513
+ `;
514
+
515
+ if (data.creation_date) {
516
+ const date = new Date(data.creation_date.date);
517
+ html += `
518
+ <div class="metadata-item">
519
+ <strong>Source</strong>
520
+ <span>${data.creation_date.source}</span>
521
+ </div>
522
+ <div class="metadata-item">
523
+ <strong>Date & Time</strong>
524
+ <span>${date.toLocaleString()}</span>
525
+ </div>
526
+ `;
527
+ } else if (data.file_system_creation_time) {
528
+ const date = new Date(data.file_system_creation_time);
529
+ html += `
530
+ <div class="metadata-item">
531
+ <strong>Source</strong>
532
+ <span>File System</span>
533
+ </div>
534
+ <div class="metadata-item">
535
+ <strong>Date & Time</strong>
536
+ <span>${date.toLocaleString()}</span>
537
+ </div>
538
+ `;
539
+ } else {
540
+ html += `
541
+ <div class="metadata-item error">
542
+ <strong>No Creation Date Found</strong>
543
+ <span>Neither EXIF data nor file system timestamps contain creation information</span>
544
+ </div>
545
+ `;
546
+ }
547
+
548
+ html += `</div>`;
549
+
550
+ // EXIF data card
551
+ html += `
552
+ <div class="metadata-card exif">
553
+ <h3>🔍 EXIF Metadata</h3>
554
+ `;
555
+
556
+ if (Object.keys(data.exif_data).length > 0) {
557
+ html += `<div class="exif-list">`;
558
+ for (const [key, value] of Object.entries(data.exif_data)) {
559
+ html += `
560
+ <div class="exif-item">
561
+ <span class="exif-key">${key}</span>
562
+ <span class="exif-value">${value}</span>
563
+ </div>
564
+ `;
565
+ }
566
+ html += `</div>`;
567
+ } else {
568
+ html += `
569
+ <div class="metadata-item">
570
+ <strong>No EXIF Data</strong>
571
+ <span>This image doesn't contain EXIF metadata</span>
572
+ </div>
573
+ `;
574
+ }
575
+
576
+ html += `</div>`;
577
+
578
+ // Error card (if any)
579
+ if (data.error) {
580
+ html += `
581
+ <div class="metadata-card">
582
+ <h3>⚠️ Error</h3>
583
+ <div class="metadata-item error">
584
+ <strong>Processing Error</strong>
585
+ <span>${data.error}</span>
586
+ </div>
587
+ </div>
588
+ `;
589
+ }
590
+
591
+ metadataContent.innerHTML = html;
592
+ }
593
+
594
+ function showError(message) {
595
+ // Hide upload area and show results
596
+ uploadArea.style.display = 'none';
597
+ resultContainer.style.display = 'block';
598
+
599
+ metadataContent.innerHTML = `
600
+ <div class="metadata-card">
601
+ <h3>⚠️ Error</h3>
602
+ <div class="metadata-item error">
603
+ <strong>Processing Failed</strong>
604
+ <span>${message}</span>
605
+ </div>
606
+ </div>
607
+ `;
608
+ }
609
+
610
+ function resetForm() {
611
+ // Show upload area and hide results
612
+ uploadArea.style.display = 'block';
613
+ resultContainer.style.display = 'none';
614
+
615
+ // Reset file input
616
+ fileInput.value = '';
617
+ }
618
+ </script>
619
+ </body>
620
+ </html>
621
+ """
622
+
623
+ @app.post("/extract-metadata")
624
+ async def extract_metadata(file: UploadFile = File(...)):
625
+ """Endpoint to extract metadata from uploaded image"""
626
+ if not file.content_type.startswith("image/"):
627
+ raise HTTPException(status_code=400, detail="File must be an image")
628
+
629
+ # Initialize tmp_file_path to None
630
+ tmp_file_path = None
631
+
632
+ try:
633
+ # Read the file content
634
+ contents = await file.read()
635
+
636
+ # Create a temporary file to store the uploaded image
637
+ with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
638
+ tmp_file.write(contents)
639
+ tmp_file_path = tmp_file.name
640
+
641
+ # Extract metadata
642
+ metadata = get_image_metadata(tmp_file_path)
643
+
644
+ return metadata
645
+ except Exception as e:
646
+ raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)}")
647
+ finally:
648
+ # Clean up temporary file
649
+ if tmp_file_path and os.path.exists(tmp_file_path):
650
+ os.unlink(tmp_file_path)
651
+
652
+ if __name__ == "__main__":
653
+ import uvicorn
 
 
 
654
  uvicorn.run(app, host="0.0.0.0", port=8000)