| from fastapi import FastAPI, File, UploadFile, HTTPException |
| from fastapi.responses import HTMLResponse |
| from PIL import Image |
| from PIL.ExifTags import TAGS |
| from datetime import datetime |
| import os |
| import tempfile |
|
|
| app = FastAPI(title="Image Metadata Extractor", description="Upload an image to extract its metadata including creation timestamp") |
|
|
| def get_image_metadata(image_path): |
| """Extract metadata from an image file""" |
| result = { |
| "filename": os.path.basename(image_path), |
| "format": None, |
| "size": None, |
| "mode": None, |
| "exif_data": {}, |
| "creation_date": None, |
| "file_system_creation_time": None, |
| "error": None |
| } |
| |
| try: |
| |
| with Image.open(image_path) as img: |
| result["format"] = img.format |
| result["size"] = img.size |
| result["mode"] = img.mode |
|
|
| |
| exif_data = img._getexif() |
| if exif_data is not None: |
| exif_dict = {} |
| for tag_id, value in exif_data.items(): |
| tag = TAGS.get(tag_id, tag_id) |
| exif_dict[tag] = str(value) |
| result["exif_data"] = exif_dict |
|
|
| |
| date_tags = ['DateTime', 'DateTimeOriginal', 'DateTimeDigitized'] |
| for tag in date_tags: |
| |
| tag_id = None |
| for tid, tname in TAGS.items(): |
| if tname == tag: |
| tag_id = tid |
| break |
| |
| if tag_id and tag_id in exif_data: |
| raw_date = exif_data[tag_id] |
| try: |
| |
| if isinstance(raw_date, bytes): |
| raw_date = raw_date.decode('utf-8') |
| |
| |
| formats_to_try = [ |
| '%Y:%m:%d %H:%M:%S', |
| '%Y-%m-%d %H:%M:%S', |
| '%Y/%m/%d %H:%M:%S' |
| ] |
| |
| creation_date = None |
| for fmt in formats_to_try: |
| try: |
| creation_date = datetime.strptime(str(raw_date), fmt) |
| break |
| except ValueError: |
| continue |
| |
| if creation_date: |
| result["creation_date"] = { |
| "source": tag, |
| "date": creation_date.isoformat() |
| } |
| break |
| except Exception: |
| continue |
|
|
| |
| stat = os.stat(image_path) |
| creation_time = stat.st_ctime |
| result["file_system_creation_time"] = datetime.fromtimestamp(creation_time).isoformat() |
|
|
| except Exception as e: |
| result["error"] = str(e) |
| |
| return result |
|
|
| @app.get("/", response_class=HTMLResponse) |
| async def read_root(): |
| """Serve the HTML frontend""" |
| return """ |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>Image Metadata Extractor</title> |
| <style> |
| :root { |
| --primary-color: #4361ee; |
| --secondary-color: #3f37c9; |
| --accent-color: #4895ef; |
| --success-color: #4cc9f0; |
| --light-color: #f8f9fa; |
| --dark-color: #212529; |
| --danger-color: #f72585; |
| --warning-color: #f8961e; |
| --info-color: #56cfe1; |
| } |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| } |
| |
| body { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| min-height: 100vh; |
| padding: 20px; |
| } |
| |
| .container { |
| max-width: 1000px; |
| margin: 0 auto; |
| } |
| |
| header { |
| text-align: center; |
| padding: 30px 0; |
| color: white; |
| text-shadow: 0 2px 4px rgba(0,0,0,0.3); |
| } |
| |
| h1 { |
| font-size: 2.5rem; |
| margin-bottom: 10px; |
| } |
| |
| .subtitle { |
| font-size: 1.2rem; |
| opacity: 0.9; |
| } |
| |
| .card { |
| background: rgba(255, 255, 255, 0.95); |
| border-radius: 15px; |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); |
| padding: 30px; |
| margin-bottom: 30px; |
| backdrop-filter: blur(10px); |
| } |
| |
| .upload-area { |
| border: 3px dashed var(--accent-color); |
| border-radius: 12px; |
| padding: 40px 20px; |
| text-align: center; |
| transition: all 0.3s ease; |
| background: rgba(72, 149, 239, 0.05); |
| cursor: pointer; |
| } |
| |
| .upload-area:hover { |
| border-color: var(--primary-color); |
| background: rgba(67, 97, 238, 0.1); |
| transform: translateY(-2px); |
| } |
| |
| .upload-area.active { |
| border-color: var(--success-color); |
| background: rgba(76, 201, 240, 0.1); |
| } |
| |
| .upload-icon { |
| font-size: 4rem; |
| color: var(--accent-color); |
| margin-bottom: 20px; |
| } |
| |
| .upload-text { |
| font-size: 1.3rem; |
| color: var(--dark-color); |
| margin-bottom: 15px; |
| } |
| |
| .upload-hint { |
| color: #6c757d; |
| margin-bottom: 20px; |
| } |
| |
| .file-input { |
| display: none; |
| } |
| |
| .upload-btn { |
| background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); |
| color: white; |
| border: none; |
| padding: 15px 40px; |
| font-size: 1.1rem; |
| border-radius: 50px; |
| cursor: pointer; |
| transition: all 0.3s ease; |
| box-shadow: 0 4px 15px rgba(67, 97, 238, 0.3); |
| } |
| |
| .upload-btn:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 6px 20px rgba(67, 97, 238, 0.4); |
| } |
| |
| .upload-btn:active { |
| transform: translateY(0); |
| } |
| |
| .result-container { |
| display: none; |
| } |
| |
| .result-header { |
| text-align: center; |
| margin-bottom: 25px; |
| color: var(--dark-color); |
| } |
| |
| .metadata-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); |
| gap: 20px; |
| margin-bottom: 30px; |
| } |
| |
| .metadata-card { |
| background: white; |
| border-radius: 10px; |
| padding: 20px; |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); |
| border-left: 4px solid var(--primary-color); |
| transition: transform 0.3s ease; |
| } |
| |
| .metadata-card:hover { |
| transform: translateY(-5px); |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); |
| } |
| |
| .metadata-card.creation { |
| border-left-color: var(--success-color); |
| } |
| |
| .metadata-card.exif { |
| border-left-color: var(--warning-color); |
| } |
| |
| .metadata-card h3 { |
| color: var(--primary-color); |
| margin-bottom: 15px; |
| font-size: 1.3rem; |
| } |
| |
| .metadata-card.creation h3 { |
| color: var(--success-color); |
| } |
| |
| .metadata-card.exif h3 { |
| color: var(--warning-color); |
| } |
| |
| .metadata-item { |
| margin: 12px 0; |
| padding: 10px; |
| background: #f8f9fa; |
| border-radius: 8px; |
| border-left: 3px solid var(--accent-color); |
| } |
| |
| .metadata-item strong { |
| display: block; |
| color: var(--dark-color); |
| margin-bottom: 5px; |
| } |
| |
| .metadata-item span { |
| color: #6c757d; |
| } |
| |
| .exif-list { |
| max-height: 300px; |
| overflow-y: auto; |
| } |
| |
| .exif-item { |
| display: flex; |
| justify-content: space-between; |
| padding: 8px 0; |
| border-bottom: 1px solid #eee; |
| } |
| |
| .exif-item:last-child { |
| border-bottom: none; |
| } |
| |
| .exif-key { |
| font-weight: 500; |
| color: var(--dark-color); |
| } |
| |
| .exif-value { |
| color: #6c757d; |
| text-align: right; |
| max-width: 60%; |
| word-break: break-word; |
| } |
| |
| .error { |
| background: #ffebee; |
| border-left-color: var(--danger-color); |
| color: var(--danger-color); |
| } |
| |
| .error strong { |
| color: var(--danger-color); |
| } |
| |
| .actions { |
| text-align: center; |
| margin-top: 20px; |
| } |
| |
| .reset-btn { |
| background: transparent; |
| color: var(--primary-color); |
| border: 2px solid var(--primary-color); |
| padding: 12px 30px; |
| font-size: 1rem; |
| border-radius: 50px; |
| cursor: pointer; |
| transition: all 0.3s ease; |
| } |
| |
| .reset-btn:hover { |
| background: var(--primary-color); |
| color: white; |
| } |
| |
| footer { |
| text-align: center; |
| color: rgba(255, 255, 255, 0.8); |
| padding: 20px 0; |
| font-size: 0.9rem; |
| } |
| |
| @media (max-width: 768px) { |
| .metadata-grid { |
| grid-template-columns: 1fr; |
| } |
| |
| h1 { |
| font-size: 2rem; |
| } |
| |
| .card { |
| padding: 20px; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <header> |
| <h1>📸 Image Metadata Extractor</h1> |
| <p class="subtitle">Upload any image to extract detailed metadata including creation timestamps</p> |
| </header> |
| |
| <main> |
| <div class="card"> |
| <div class="upload-area" id="upload-area"> |
| <div class="upload-icon">📁</div> |
| <h2 class="upload-text">Drag & Drop your image here</h2> |
| <p class="upload-hint">or click to browse files</p> |
| <input type="file" id="file-input" class="file-input" accept="image/*"> |
| <button class="upload-btn" id="upload-btn">Select Image</button> |
| </div> |
| </div> |
| |
| <div class="card result-container" id="result-container"> |
| <h2 class="result-header">Image Metadata Analysis</h2> |
| <div class="metadata-grid" id="metadata-content"></div> |
| <div class="actions"> |
| <button class="reset-btn" id="reset-btn">Analyze Another Image</button> |
| </div> |
| </div> |
| </main> |
| |
| <footer> |
| <p>Image Metadata Extractor © 2025 | Extract creation timestamps and EXIF data</p> |
| </footer> |
| </div> |
| |
| <script> |
| const uploadArea = document.getElementById('upload-area'); |
| const fileInput = document.getElementById('file-input'); |
| const uploadBtn = document.getElementById('upload-btn'); |
| const resultContainer = document.getElementById('result-container'); |
| const metadataContent = document.getElementById('metadata-content'); |
| const resetBtn = document.getElementById('reset-btn'); |
| |
| // Event listeners |
| uploadBtn.addEventListener('click', () => fileInput.click()); |
| fileInput.addEventListener('change', handleFileSelect); |
| uploadArea.addEventListener('dragover', handleDragOver); |
| uploadArea.addEventListener('dragleave', handleDragLeave); |
| uploadArea.addEventListener('drop', handleDrop); |
| resetBtn.addEventListener('click', resetForm); |
| |
| function handleDragOver(e) { |
| e.preventDefault(); |
| uploadArea.classList.add('active'); |
| } |
| |
| function handleDragLeave() { |
| uploadArea.classList.remove('active'); |
| } |
| |
| function handleDrop(e) { |
| e.preventDefault(); |
| uploadArea.classList.remove('active'); |
| |
| if (e.dataTransfer.files.length) { |
| fileInput.files = e.dataTransfer.files; |
| processFile(fileInput.files[0]); |
| } |
| } |
| |
| function handleFileSelect() { |
| if (fileInput.files.length) { |
| processFile(fileInput.files[0]); |
| } |
| } |
| |
| async function processFile(file) { |
| if (!file.type.startsWith('image/')) { |
| alert('Please select an image file'); |
| return; |
| } |
| |
| const formData = new FormData(); |
| formData.append('file', file); |
| |
| try { |
| // Show loading state |
| uploadBtn.textContent = 'Processing...'; |
| uploadBtn.disabled = true; |
| |
| const response = await fetch('/extract-metadata', { |
| method: 'POST', |
| body: formData |
| }); |
| |
| const data = await response.json(); |
| |
| if (response.ok) { |
| displayMetadata(data); |
| } else { |
| showError(data.detail || 'Error processing image'); |
| } |
| } catch (error) { |
| showError('Error: ' + error.message); |
| } finally { |
| uploadBtn.textContent = 'Select Image'; |
| uploadBtn.disabled = false; |
| } |
| } |
| |
| function displayMetadata(data) { |
| // Hide upload area and show results |
| uploadArea.style.display = 'none'; |
| resultContainer.style.display = 'block'; |
| |
| // Generate metadata cards |
| let html = ''; |
| |
| // Basic info card |
| html += ` |
| <div class="metadata-card"> |
| <h3>📋 Basic Information</h3> |
| <div class="metadata-item"> |
| <strong>Filename</strong> |
| <span>${data.filename}</span> |
| </div> |
| <div class="metadata-item"> |
| <strong>Format</strong> |
| <span>${data.format || 'Unknown'}</span> |
| </div> |
| <div class="metadata-item"> |
| <strong>Dimensions</strong> |
| <span>${data.size ? `${data.size[0]} × ${data.size[1]} pixels` : 'Unknown'}</span> |
| </div> |
| <div class="metadata-item"> |
| <strong>Color Mode</strong> |
| <span>${data.mode || 'Unknown'}</span> |
| </div> |
| </div> |
| `; |
| |
| // Creation date card |
| html += ` |
| <div class="metadata-card creation"> |
| <h3>⏱️ Creation Timestamp</h3> |
| `; |
| |
| if (data.creation_date) { |
| const date = new Date(data.creation_date.date); |
| html += ` |
| <div class="metadata-item"> |
| <strong>Source</strong> |
| <span>${data.creation_date.source}</span> |
| </div> |
| <div class="metadata-item"> |
| <strong>Date & Time</strong> |
| <span>${date.toLocaleString()}</span> |
| </div> |
| `; |
| } else if (data.file_system_creation_time) { |
| const date = new Date(data.file_system_creation_time); |
| html += ` |
| <div class="metadata-item"> |
| <strong>Source</strong> |
| <span>File System</span> |
| </div> |
| <div class="metadata-item"> |
| <strong>Date & Time</strong> |
| <span>${date.toLocaleString()}</span> |
| </div> |
| `; |
| } else { |
| html += ` |
| <div class="metadata-item error"> |
| <strong>No Creation Date Found</strong> |
| <span>Neither EXIF data nor file system timestamps contain creation information</span> |
| </div> |
| `; |
| } |
| |
| html += `</div>`; |
| |
| // EXIF data card |
| html += ` |
| <div class="metadata-card exif"> |
| <h3>🔍 EXIF Metadata</h3> |
| `; |
| |
| if (Object.keys(data.exif_data).length > 0) { |
| html += `<div class="exif-list">`; |
| for (const [key, value] of Object.entries(data.exif_data)) { |
| html += ` |
| <div class="exif-item"> |
| <span class="exif-key">${key}</span> |
| <span class="exif-value">${value}</span> |
| </div> |
| `; |
| } |
| html += `</div>`; |
| } else { |
| html += ` |
| <div class="metadata-item"> |
| <strong>No EXIF Data</strong> |
| <span>This image doesn't contain EXIF metadata</span> |
| </div> |
| `; |
| } |
| |
| html += `</div>`; |
| |
| // Error card (if any) |
| if (data.error) { |
| html += ` |
| <div class="metadata-card"> |
| <h3>⚠️ Error</h3> |
| <div class="metadata-item error"> |
| <strong>Processing Error</strong> |
| <span>${data.error}</span> |
| </div> |
| </div> |
| `; |
| } |
| |
| metadataContent.innerHTML = html; |
| } |
| |
| function showError(message) { |
| // Hide upload area and show results |
| uploadArea.style.display = 'none'; |
| resultContainer.style.display = 'block'; |
| |
| metadataContent.innerHTML = ` |
| <div class="metadata-card"> |
| <h3>⚠️ Error</h3> |
| <div class="metadata-item error"> |
| <strong>Processing Failed</strong> |
| <span>${message}</span> |
| </div> |
| </div> |
| `; |
| } |
| |
| function resetForm() { |
| // Show upload area and hide results |
| uploadArea.style.display = 'block'; |
| resultContainer.style.display = 'none'; |
| |
| // Reset file input |
| fileInput.value = ''; |
| } |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| @app.post("/extract-metadata") |
| async def extract_metadata(file: UploadFile = File(...)): |
| """Endpoint to extract metadata from uploaded image""" |
| if not file.content_type.startswith("image/"): |
| raise HTTPException(status_code=400, detail="File must be an image") |
| |
| |
| tmp_file_path = None |
| |
| try: |
| |
| contents = await file.read() |
| |
| |
| with tempfile.NamedTemporaryFile(delete=False) as tmp_file: |
| tmp_file.write(contents) |
| tmp_file_path = tmp_file.name |
| |
| |
| metadata = get_image_metadata(tmp_file_path) |
| |
| return metadata |
| except Exception as e: |
| raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)}") |
| finally: |
| |
| if tmp_file_path and os.path.exists(tmp_file_path): |
| os.unlink(tmp_file_path) |
|
|
| if __name__ == "__main__": |
| import uvicorn |
| uvicorn.run(app, host="0.0.0.0", port=8000) |