Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>CMS - DICI/SAMU IMAGE PERFECT</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: #0f172a; | |
| color: #e2e8f0; | |
| } | |
| .dropzone { | |
| border: 2px dashed #3b82f6; | |
| transition: all 0.3s ease; | |
| } | |
| .dropzone.active { | |
| border-color: #10b981; | |
| background-color: rgba(16, 185, 129, 0.05); | |
| } | |
| .progress-bar { | |
| height: 6px; | |
| background-color: #1e40af; | |
| transition: width 0.3s ease; | |
| } | |
| .slider-thumb::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #3b82f6; | |
| cursor: pointer; | |
| } | |
| .slider-thumb::-moz-range-thumb { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: #3b82f6; | |
| cursor: pointer; | |
| } | |
| .result-image { | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3); | |
| transition: all 0.3s ease; | |
| max-height: 300px; | |
| object-fit: contain; | |
| } | |
| .result-image:hover { | |
| transform: scale(1.02); | |
| } | |
| .tooltip { | |
| position: relative; | |
| } | |
| .tooltip-text { | |
| visibility: hidden; | |
| width: 120px; | |
| background-color: #1e293b; | |
| color: #fff; | |
| text-align: center; | |
| border-radius: 6px; | |
| padding: 5px; | |
| position: absolute; | |
| z-index: 1; | |
| bottom: 125%; | |
| left: 50%; | |
| margin-left: -60px; | |
| opacity: 0; | |
| transition: opacity 0.3s; | |
| } | |
| .tooltip:hover .tooltip-text { | |
| visibility: visible; | |
| opacity: 1; | |
| } | |
| .preview-container { | |
| position: relative; | |
| background-color: #1e293b; | |
| background-image: | |
| linear-gradient(45deg, #334155 25%, transparent 25%), | |
| linear-gradient(-45deg, #334155 25%, transparent 25%), | |
| linear-gradient(45deg, transparent 75%, #334155 75%), | |
| linear-gradient(-45deg, transparent 75%, #334155 75%); | |
| background-size: 20px 20px; | |
| background-position: 0 0, 0 10px, 10px -10px, -10px 0px; | |
| } | |
| .model-btn.active { | |
| background-color: #3b82f6; | |
| color: white; | |
| } | |
| /* Modal styles */ | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.7); | |
| z-index: 1000; | |
| overflow-y: auto; | |
| } | |
| .modal-content { | |
| background-color: #1e293b; | |
| margin: 5% auto; | |
| padding: 20px; | |
| border-radius: 10px; | |
| width: 90%; | |
| max-width: 800px; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); | |
| } | |
| .close-modal { | |
| color: #aaa; | |
| float: right; | |
| font-size: 28px; | |
| font-weight: bold; | |
| cursor: pointer; | |
| } | |
| .close-modal:hover { | |
| color: #fff; | |
| } | |
| .batch-preview { | |
| max-height: 300px; | |
| overflow-y: auto; | |
| margin-bottom: 20px; | |
| border: 1px solid #334155; | |
| border-radius: 5px; | |
| padding: 10px; | |
| } | |
| .batch-item { | |
| display: flex; | |
| align-items: center; | |
| padding: 8px; | |
| border-bottom: 1px solid #334155; | |
| } | |
| .batch-item:last-child { | |
| border-bottom: none; | |
| } | |
| .batch-thumbnail { | |
| width: 50px; | |
| height: 50px; | |
| object-fit: cover; | |
| margin-right: 10px; | |
| border-radius: 3px; | |
| } | |
| .batch-info { | |
| flex-grow: 1; | |
| } | |
| .batch-status { | |
| margin-left: 10px; | |
| font-size: 12px; | |
| padding: 3px 6px; | |
| border-radius: 3px; | |
| } | |
| .status-pending { | |
| background-color: #3b82f6; | |
| } | |
| .status-completed { | |
| background-color: #10b981; | |
| } | |
| .status-error { | |
| background-color: #ef4444; | |
| } | |
| .batch-actions { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 20px; | |
| } | |
| .preset-option { | |
| transition: all 0.2s ease; | |
| cursor: pointer; | |
| } | |
| .preset-option:hover { | |
| transform: translateY(-2px); | |
| } | |
| .preset-option.selected { | |
| border: 2px solid #3b82f6; | |
| box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen"> | |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> | |
| <!-- Header --> | |
| <header class="flex justify-between items-center mb-10"> | |
| <div class="flex items-center space-x-2"> | |
| <i class="fas fa-expand text-blue-500 text-2xl"></i> | |
| <h1 class="text-2xl font-bold bg-gradient-to-r from-blue-500 to-emerald-500 bg-clip-text text-transparent">CMS - DICI/SAMU IMAGE PERFECT</h1> | |
| </div> | |
| <div class="flex space-x-4"> | |
| <button id="batchBtn" class="px-4 py-2 rounded-full bg-slate-800 hover:bg-slate-700 transition-colors"> | |
| <i class="fas fa-layer-group mr-2"></i>Batch Resize | |
| </button> | |
| <button class="px-4 py-2 rounded-full bg-slate-800 hover:bg-slate-700 transition-colors"> | |
| <i class="fas fa-question-circle mr-2"></i>Help | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> | |
| <!-- Upload Section --> | |
| <div class="bg-slate-800 rounded-xl p-6 shadow-lg"> | |
| <h2 class="text-xl font-semibold mb-4">Upload Image</h2> | |
| <p class="text-slate-400 mb-6">Enhance your images with AI-powered upscaling. Supports JPG, PNG up to 10MB.</p> | |
| <div id="dropzone" class="dropzone rounded-xl p-8 text-center cursor-pointer mb-6"> | |
| <div id="uploadContent" class="flex flex-col items-center justify-center space-y-3"> | |
| <i class="fas fa-cloud-upload-alt text-blue-500 text-4xl"></i> | |
| <p class="font-medium">Drag & drop your image here</p> | |
| <p class="text-slate-400 text-sm">or click to browse files</p> | |
| <input type="file" id="fileInput" class="hidden" accept="image/*"> | |
| </div> | |
| <div id="imagePreview" class="hidden"> | |
| <div class="preview-container rounded-lg overflow-hidden mb-3"> | |
| <img id="previewImage" src="" alt="Preview" class="w-full h-auto max-h-64 mx-auto"> | |
| </div> | |
| <p id="fileName" class="text-sm font-medium truncate max-w-full"></p> | |
| <p id="fileSize" class="text-xs text-slate-400"></p> | |
| </div> | |
| </div> | |
| <div class="flex justify-between items-center mb-6"> | |
| <div> | |
| <h3 class="font-medium mb-1">Upscale Settings</h3> | |
| <p class="text-slate-400 text-sm">Adjust quality and scale</p> | |
| </div> | |
| <button id="resetBtn" class="px-3 py-1 rounded-md bg-slate-700 hover:bg-slate-600 text-sm transition-colors"> | |
| <i class="fas fa-redo mr-1"></i>Reset | |
| </button> | |
| </div> | |
| <!-- Settings --> | |
| <div class="space-y-5"> | |
| <div> | |
| <div class="flex justify-between mb-2"> | |
| <label class="font-medium">Scale Factor</label> | |
| <span id="scaleValue" class="text-blue-400">2x</span> | |
| </div> | |
| <input type="range" id="scaleSlider" min="1" max="4" step="1" value="2" class="w-full slider-thumb"> | |
| <div class="flex justify-between text-xs text-slate-400 mt-1"> | |
| <span>1x</span> | |
| <span>2x</span> | |
| <span>3x</span> | |
| <span>4x</span> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="flex justify-between mb-2"> | |
| <label class="font-medium">AI Model</label> | |
| <span id="modelValue" class="text-blue-400">Standard</span> | |
| </div> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <button id="standardModel" class="py-2 px-1 rounded-md bg-blue-600 hover:bg-blue-700 transition-colors text-sm active"> | |
| Standard | |
| </button> | |
| <button id="enhancedModel" class="py-2 px-1 rounded-md bg-slate-700 hover:bg-slate-600 transition-colors text-sm"> | |
| Enhanced | |
| </button> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="flex justify-between mb-2"> | |
| <label class="font-medium">Noise Reduction</label> | |
| <span id="noiseValue" class="text-blue-400">50%</span> | |
| </div> | |
| <input type="range" id="noiseSlider" min="0" max="100" value="50" class="w-full slider-thumb"> | |
| </div> | |
| </div> | |
| <button id="processBtn" class="w-full mt-8 py-3 rounded-xl bg-gradient-to-r from-blue-600 to-emerald-600 hover:from-blue-700 hover:to-emerald-700 font-medium transition-all transform hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed" disabled> | |
| <i class="fas fa-magic mr-2"></i>Upscale Image | |
| </button> | |
| </div> | |
| <!-- Results Section --> | |
| <div class="bg-slate-800 rounded-xl p-6 shadow-lg"> | |
| <h2 class="text-xl font-semibold mb-4">Results</h2> | |
| <p class="text-slate-400 mb-6">Your enhanced images will appear here after processing.</p> | |
| <div id="resultContainer" class="hidden"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <div> | |
| <h3 class="font-medium">Comparison</h3> | |
| <p class="text-slate-400 text-sm">Original vs Upscaled</p> | |
| </div> | |
| <div class="flex space-x-2"> | |
| <button id="downloadBtn" class="p-2 rounded-md bg-slate-700 hover:bg-slate-600 transition-colors tooltip"> | |
| <i class="fas fa-download"></i> | |
| <span class="tooltip-text">Download</span> | |
| </button> | |
| <button class="p-2 rounded-md bg-slate-700 hover:bg-slate-600 transition-colors tooltip"> | |
| <i class="fas fa-copy"></i> | |
| <span class="tooltip-text">Copy</span> | |
| </button> | |
| <button class="p-2 rounded-md bg-slate-700 hover:bg-slate-600 transition-colors tooltip"> | |
| <i class="fas fa-share-alt"></i> | |
| <span class="tooltip-text">Share</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-2 gap-4 mb-6"> | |
| <div> | |
| <div class="preview-container rounded-lg overflow-hidden mb-2"> | |
| <img id="originalImage" src="" alt="Original" class="w-full h-auto result-image"> | |
| </div> | |
| <p class="text-center text-sm text-slate-400">Original</p> | |
| </div> | |
| <div> | |
| <div class="preview-container rounded-lg overflow-hidden mb-2 relative"> | |
| <img id="upscaledImage" src="" alt="Upscaled" class="w-full h-auto result-image"> | |
| <div class="absolute top-2 right-2 bg-blue-600 text-white text-xs px-2 py-1 rounded-full"> | |
| <span id="scaleBadge">2x</span> | |
| </div> | |
| </div> | |
| <p class="text-center text-sm text-slate-400">Upscaled</p> | |
| </div> | |
| </div> | |
| <div class="bg-slate-900 rounded-lg p-4"> | |
| <div class="flex justify-between items-center mb-2"> | |
| <span class="font-medium">Details</span> | |
| <span class="text-xs text-slate-400">Processing time: <span id="processingTime">1.4</span>s</span> | |
| </div> | |
| <div class="grid grid-cols-3 gap-2 text-center text-sm"> | |
| <div class="bg-slate-800 p-2 rounded"> | |
| <div class="text-slate-400">Original Size</div> | |
| <div id="originalSize" class="font-medium">1.2 MB</div> | |
| </div> | |
| <div class="bg-slate-800 p-2 rounded"> | |
| <div class="text-slate-400">New Size</div> | |
| <div id="newSize" class="font-medium">3.8 MB</div> | |
| </div> | |
| <div class="bg-slate-800 p-2 rounded"> | |
| <div class="text-slate-400">Resolution</div> | |
| <div id="resolution" class="font-medium">1920×1080 → 3840×2160</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="emptyState" class="flex flex-col items-center justify-center py-12"> | |
| <div class="bg-slate-900 rounded-full p-6 mb-4"> | |
| <i class="fas fa-image text-3xl text-slate-600"></i> | |
| </div> | |
| <h3 class="font-medium mb-1">No image processed yet</h3> | |
| <p class="text-slate-400 text-sm">Upload an image and click "Upscale Image"</p> | |
| </div> | |
| <div id="progressContainer" class="hidden"> | |
| <div class="flex flex-col items-center justify-center py-12 space-y-4"> | |
| <div class="relative"> | |
| <div class="w-16 h-16 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div> | |
| <i class="fas fa-magic text-blue-500 text-xl absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"></i> | |
| </div> | |
| <h3 class="font-medium">Processing your image</h3> | |
| <p class="text-slate-400 text-sm">This may take a few seconds...</p> | |
| <div class="w-full bg-slate-700 rounded-full h-1.5"> | |
| <div id="progressBar" class="progress-bar h-1.5 rounded-full" style="width: 0%"></div> | |
| </div> | |
| <p id="progressText" class="text-xs text-slate-400">0% completed</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="mt-16 text-center text-slate-400 text-sm"> | |
| <p>© 2025 CMS - DICI/SAMU IMAGE PERFECT. All rights reserved.</p> | |
| </footer> | |
| </div> | |
| <!-- Batch Resize Modal --> | |
| <div id="batchModal" class="modal"> | |
| <div class="modal-content"> | |
| <span class="close-modal">×</span> | |
| <h2 class="text-xl font-semibold mb-4">Batch Image Resize</h2> | |
| <p class="text-slate-400 mb-6">Resize multiple images to standard dimensions in one operation.</p> | |
| <div class="dropzone rounded-xl p-8 text-center cursor-pointer mb-6" id="batchDropzone"> | |
| <div id="batchUploadContent" class="flex flex-col items-center justify-center space-y-3"> | |
| <i class="fas fa-layer-group text-blue-500 text-4xl"></i> | |
| <p class="font-medium">Drag & drop your images here</p> | |
| <p class="text-slate-400 text-sm">or click to browse files (max 20 images)</p> | |
| <input type="file" id="batchFileInput" class="hidden" accept="image/*" multiple> | |
| </div> | |
| </div> | |
| <div id="batchPreview" class="batch-preview hidden"> | |
| <!-- Preview items will be added here --> | |
| </div> | |
| <div class="space-y-5"> | |
| <h3 class="font-medium">Resize Presets</h3> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div id="eviarPreset" class="preset-option bg-slate-800 p-4 rounded-lg selected"> | |
| <div class="flex items-center mb-3"> | |
| <input type="radio" name="preset" value="eviar" class="mr-2" checked> | |
| <label class="font-medium">EVIAR</label> | |
| </div> | |
| <p class="text-slate-400 text-sm">32.4cm × 21.6cm @ 300dpi</p> | |
| <p class="text-slate-400 text-xs">(3827 × 2551 pixels)</p> | |
| </div> | |
| <div id="sitePreset" class="preset-option bg-slate-800 p-4 rounded-lg"> | |
| <div class="flex items-center mb-3"> | |
| <input type="radio" name="preset" value="site" class="mr-2"> | |
| <label class="font-medium">SITE</label> | |
| </div> | |
| <p class="text-slate-400 text-sm">1000px × 667px @ 72dpi</p> | |
| <p class="text-slate-400 text-xs">(Optimized for web)</p> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="flex justify-between mb-2"> | |
| <label class="font-medium">JPEG Quality</label> | |
| <span id="qualityValue" class="text-blue-400">85%</span> | |
| </div> | |
| <input type="range" id="qualitySlider" min="50" max="100" value="85" class="w-full slider-thumb"> | |
| </div> | |
| </div> | |
| <div class="batch-actions"> | |
| <button id="cancelBatchBtn" class="px-4 py-2 rounded-md bg-slate-700 hover:bg-slate-600 transition-colors"> | |
| Cancel | |
| </button> | |
| <button id="processBatchBtn" class="px-4 py-2 rounded-md bg-gradient-to-r from-blue-600 to-emerald-600 hover:from-blue-700 hover:to-emerald-700 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed" disabled> | |
| <i class="fas fa-cogs mr-2"></i>Process Batch | |
| </button> | |
| </div> | |
| <div id="batchProgressContainer" class="hidden mt-6"> | |
| <h4 class="font-medium mb-2">Processing Progress</h4> | |
| <div class="w-full bg-slate-700 rounded-full h-2.5 mb-2"> | |
| <div id="batchProgressBar" class="progress-bar h-2.5 rounded-full" style="width: 0%"></div> | |
| </div> | |
| <div class="flex justify-between text-sm text-slate-400"> | |
| <span id="batchProgressText">0% (0/0)</span> | |
| <span id="batchTimeRemaining">Estimated time: calculating...</span> | |
| </div> | |
| </div> | |
| <div id="batchResults" class="hidden mt-6"> | |
| <h4 class="font-medium mb-2">Results</h4> | |
| <div class="bg-slate-900 rounded-lg p-4"> | |
| <div class="grid grid-cols-3 gap-2 text-center text-sm"> | |
| <div class="bg-slate-800 p-2 rounded"> | |
| <div class="text-slate-400">Processed</div> | |
| <div id="processedCount" class="font-medium">0</div> | |
| </div> | |
| <div class="bg-slate-800 p-2 rounded"> | |
| <div class="text-slate-400">Success</div> | |
| <div id="successCount" class="font-medium">0</div> | |
| </div> | |
| <div class="bg-slate-800 p-2 rounded"> | |
| <div class="text-slate-400">Errors</div> | |
| <div id="errorCount" class="font-medium">0</div> | |
| </div> | |
| </div> | |
| </div> | |
| <button id="downloadBatchBtn" class="w-full mt-4 py-2 rounded-md bg-gradient-to-r from-blue-600 to-emerald-600 hover:from-blue-700 hover:to-emerald-700 font-medium transition-colors"> | |
| <i class="fas fa-file-archive mr-2"></i>Download All as ZIP | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // DOM Elements | |
| const dropzone = document.getElementById('dropzone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const processBtn = document.getElementById('processBtn'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| const scaleSlider = document.getElementById('scaleSlider'); | |
| const scaleValue = document.getElementById('scaleValue'); | |
| const noiseSlider = document.getElementById('noiseSlider'); | |
| const noiseValue = document.getElementById('noiseValue'); | |
| const resultContainer = document.getElementById('resultContainer'); | |
| const emptyState = document.getElementById('emptyState'); | |
| const progressContainer = document.getElementById('progressContainer'); | |
| const originalImage = document.getElementById('originalImage'); | |
| const upscaledImage = document.getElementById('upscaledImage'); | |
| const originalSize = document.getElementById('originalSize'); | |
| const newSize = document.getElementById('newSize'); | |
| const resolution = document.getElementById('resolution'); | |
| const processingTime = document.getElementById('processingTime'); | |
| const scaleBadge = document.getElementById('scaleBadge'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const progressText = document.getElementById('progressText'); | |
| const uploadContent = document.getElementById('uploadContent'); | |
| const imagePreview = document.getElementById('imagePreview'); | |
| const previewImage = document.getElementById('previewImage'); | |
| const fileName = document.getElementById('fileName'); | |
| const fileSize = document.getElementById('fileSize'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const standardModel = document.getElementById('standardModel'); | |
| const enhancedModel = document.getElementById('enhancedModel'); | |
| const modelValue = document.getElementById('modelValue'); | |
| // Batch resize elements | |
| const batchBtn = document.getElementById('batchBtn'); | |
| const batchModal = document.getElementById('batchModal'); | |
| const closeModal = document.querySelector('.close-modal'); | |
| const cancelBatchBtn = document.getElementById('cancelBatchBtn'); | |
| const batchDropzone = document.getElementById('batchDropzone'); | |
| const batchFileInput = document.getElementById('batchFileInput'); | |
| const batchUploadContent = document.getElementById('batchUploadContent'); | |
| const batchPreview = document.getElementById('batchPreview'); | |
| const processBatchBtn = document.getElementById('processBatchBtn'); | |
| const batchProgressContainer = document.getElementById('batchProgressContainer'); | |
| const batchProgressBar = document.getElementById('batchProgressBar'); | |
| const batchProgressText = document.getElementById('batchProgressText'); | |
| const batchTimeRemaining = document.getElementById('batchTimeRemaining'); | |
| const batchResults = document.getElementById('batchResults'); | |
| const processedCount = document.getElementById('processedCount'); | |
| const successCount = document.getElementById('successCount'); | |
| const errorCount = document.getElementById('errorCount'); | |
| const downloadBatchBtn = document.getElementById('downloadBatchBtn'); | |
| const qualitySlider = document.getElementById('qualitySlider'); | |
| const qualityValue = document.getElementById('qualityValue'); | |
| const eviarPreset = document.getElementById('eviarPreset'); | |
| const sitePreset = document.getElementById('sitePreset'); | |
| // Variables | |
| let selectedFile = null; | |
| let upscaledImageBlob = null; | |
| let currentModel = 'standard'; | |
| let batchFiles = []; | |
| let batchResultsData = []; | |
| let selectedPreset = 'eviar'; // Default to EVIAR | |
| // Event Listeners | |
| dropzone.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length) { | |
| handleFileSelect(e.target.files[0]); | |
| } | |
| }); | |
| dropzone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| dropzone.classList.add('active'); | |
| }); | |
| ['dragleave', 'dragend'].forEach(type => { | |
| dropzone.addEventListener(type, () => { | |
| dropzone.classList.remove('active'); | |
| }); | |
| }); | |
| dropzone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dropzone.classList.remove('active'); | |
| if (e.dataTransfer.files.length) { | |
| handleFileSelect(e.dataTransfer.files[0]); | |
| } | |
| }); | |
| scaleSlider.addEventListener('input', () => { | |
| scaleValue.textContent = `${scaleSlider.value}x`; | |
| }); | |
| noiseSlider.addEventListener('input', () => { | |
| noiseValue.textContent = `${noiseSlider.value}%`; | |
| }); | |
| standardModel.addEventListener('click', () => { | |
| currentModel = 'standard'; | |
| modelValue.textContent = 'Standard'; | |
| standardModel.classList.add('bg-blue-600'); | |
| standardModel.classList.remove('bg-slate-700'); | |
| enhancedModel.classList.add('bg-slate-700'); | |
| enhancedModel.classList.remove('bg-blue-600'); | |
| }); | |
| enhancedModel.addEventListener('click', () => { | |
| currentModel = 'enhanced'; | |
| modelValue.textContent = 'Enhanced'; | |
| enhancedModel.classList.add('bg-blue-600'); | |
| enhancedModel.classList.remove('bg-slate-700'); | |
| standardModel.classList.add('bg-slate-700'); | |
| standardModel.classList.remove('bg-blue-600'); | |
| }); | |
| processBtn.addEventListener('click', processImage); | |
| resetBtn.addEventListener('click', resetApp); | |
| downloadBtn.addEventListener('click', downloadUpscaledImage); | |
| // Batch resize event listeners | |
| batchBtn.addEventListener('click', () => { | |
| batchModal.style.display = 'block'; | |
| }); | |
| closeModal.addEventListener('click', () => { | |
| batchModal.style.display = 'none'; | |
| }); | |
| cancelBatchBtn.addEventListener('click', () => { | |
| batchModal.style.display = 'none'; | |
| resetBatchModal(); | |
| }); | |
| batchDropzone.addEventListener('click', () => batchFileInput.click()); | |
| batchFileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length) { | |
| handleBatchFiles(e.target.files); | |
| } | |
| }); | |
| batchDropzone.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| batchDropzone.classList.add('active'); | |
| }); | |
| ['dragleave', 'dragend'].forEach(type => { | |
| batchDropzone.addEventListener(type, () => { | |
| batchDropzone.classList.remove('active'); | |
| }); | |
| }); | |
| batchDropzone.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| batchDropzone.classList.remove('active'); | |
| if (e.dataTransfer.files.length) { | |
| handleBatchFiles(e.dataTransfer.files); | |
| } | |
| }); | |
| qualitySlider.addEventListener('input', () => { | |
| qualityValue.textContent = `${qualitySlider.value}%`; | |
| }); | |
| // Preset selection | |
| eviarPreset.addEventListener('click', () => { | |
| selectedPreset = 'eviar'; | |
| eviarPreset.classList.add('selected'); | |
| sitePreset.classList.remove('selected'); | |
| document.querySelector('input[name="preset"][value="eviar"]').checked = true; | |
| }); | |
| sitePreset.addEventListener('click', () => { | |
| selectedPreset = 'site'; | |
| sitePreset.classList.add('selected'); | |
| eviarPreset.classList.remove('selected'); | |
| document.querySelector('input[name="preset"][value="site"]').checked = true; | |
| }); | |
| processBatchBtn.addEventListener('click', processBatchImages); | |
| downloadBatchBtn.addEventListener('click', downloadBatchResults); | |
| // Close modal when clicking outside | |
| window.addEventListener('click', (e) => { | |
| if (e.target === batchModal) { | |
| batchModal.style.display = 'none'; | |
| } | |
| }); | |
| // Functions | |
| function handleFileSelect(file) { | |
| // Check if file is an image | |
| if (!file.type.match('image.*')) { | |
| alert('Please select an image file (JPG, PNG)'); | |
| return; | |
| } | |
| // Check file size (max 10MB) | |
| if (file.size > 10 * 1024 * 1024) { | |
| alert('File size exceeds 10MB limit'); | |
| return; | |
| } | |
| selectedFile = file; | |
| // Display file preview | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| // Show preview | |
| uploadContent.classList.add('hidden'); | |
| imagePreview.classList.remove('hidden'); | |
| previewImage.src = e.target.result; | |
| fileName.textContent = file.name; | |
| fileSize.textContent = formatFileSize(file.size); | |
| // Set original image for comparison | |
| originalImage.src = e.target.result; | |
| // Simulate file info | |
| originalSize.textContent = formatFileSize(file.size); | |
| const dimensions = getRandomDimensions(); | |
| resolution.textContent = `${dimensions.original} → ${dimensions.upscaled}`; | |
| // Enable process button | |
| processBtn.disabled = false; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| function processImage() { | |
| if (!selectedFile) { | |
| alert('Please select an image first'); | |
| return; | |
| } | |
| // Show progress | |
| emptyState.classList.add('hidden'); | |
| resultContainer.classList.add('hidden'); | |
| progressContainer.classList.remove('hidden'); | |
| // Simulate processing | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += Math.random() * 10; | |
| if (progress > 100) progress = 100; | |
| progressBar.style.width = `${progress}%`; | |
| progressText.textContent = `${Math.round(progress)}% completed`; | |
| if (progress === 100) { | |
| clearInterval(interval); | |
| setTimeout(() => { | |
| // For demo purposes, we'll create a slightly modified version of the original | |
| createUpscaledImage(selectedFile).then(blob => { | |
| upscaledImageBlob = blob; | |
| showResults(); | |
| }); | |
| }, 500); | |
| } | |
| }, 300); | |
| } | |
| function createUpscaledImage(file) { | |
| return new Promise((resolve) => { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| // Create a canvas to "upscale" the image (just for demo) | |
| const canvas = document.createElement('canvas'); | |
| const scale = parseInt(scaleSlider.value); | |
| canvas.width = img.width * scale; | |
| canvas.height = img.height * scale; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.imageSmoothingEnabled = true; | |
| ctx.drawImage(img, 0, 0, canvas.width, canvas.height); | |
| // Apply different effects based on selected model | |
| if (currentModel === 'enhanced') { | |
| // Enhanced model adds more sharpness | |
| ctx.globalAlpha = 0.7; | |
| ctx.filter = 'contrast(1.1) saturate(1.1)'; | |
| ctx.drawImage(img, 0, 0, canvas.width, canvas.height); | |
| ctx.globalAlpha = 1; | |
| ctx.filter = 'none'; | |
| } | |
| // Apply noise reduction based on slider | |
| if (noiseSlider.value > 50) { | |
| ctx.globalAlpha = 0.5; | |
| ctx.filter = 'blur(0.5px)'; | |
| ctx.drawImage(img, 0, 0, canvas.width, canvas.height); | |
| ctx.globalAlpha = 1; | |
| ctx.filter = 'none'; | |
| } | |
| // Convert to blob | |
| canvas.toBlob(blob => { | |
| resolve(blob); | |
| }, file.type, 0.9); | |
| }; | |
| img.src = e.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |
| function showResults() { | |
| progressContainer.classList.add('hidden'); | |
| resultContainer.classList.remove('hidden'); | |
| // Create object URL for the upscaled image | |
| const upscaledUrl = URL.createObjectURL(upscaledImageBlob); | |
| upscaledImage.src = upscaledUrl; | |
| // Update info | |
| const scale = scaleSlider.value; | |
| scaleBadge.textContent = `${scale}x`; | |
| // Simulate upscaled file size (original * scale factor) | |
| const originalSizeValue = selectedFile.size; | |
| const newSizeValue = originalSizeValue * scale * (currentModel === 'enhanced' ? 1.2 : 0.8); | |
| newSize.textContent = formatFileSize(newSizeValue); | |
| // Simulate processing time (1-3 seconds for standard, 2-4 for enhanced) | |
| const processingTimeValue = currentModel === 'enhanced' | |
| ? (2 + Math.random() * 2).toFixed(1) | |
| : (1 + Math.random() * 2).toFixed(1); | |
| processingTime.textContent = processingTimeValue; | |
| } | |
| function downloadUpscaledImage() { | |
| if (!upscaledImageBlob) return; | |
| const a = document.createElement('a'); | |
| const url = URL.createObjectURL(upscaledImageBlob); | |
| const fileName = selectedFile.name.replace(/\.[^/.]+$/, '') + '_upscaled' + selectedFile.name.match(/\.[^/.]+$/)[0]; | |
| a.href = url; | |
| a.download = fileName; | |
| document.body.appendChild(a); | |
| a.click(); | |
| setTimeout(() => { | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }, 0); | |
| } | |
| function resetApp() { | |
| // Reset file input | |
| fileInput.value = ''; | |
| selectedFile = null; | |
| upscaledImageBlob = null; | |
| // Reset UI | |
| originalImage.src = ''; | |
| upscaledImage.src = ''; | |
| emptyState.classList.remove('hidden'); | |
| resultContainer.classList.add('hidden'); | |
| progressContainer.classList.add('hidden'); | |
| uploadContent.classList.remove('hidden'); | |
| imagePreview.classList.add('hidden'); | |
| previewImage.src = ''; | |
| // Reset sliders | |
| scaleSlider.value = 2; | |
| scaleValue.textContent = '2x'; | |
| noiseSlider.value = 50; | |
| noiseValue.textContent = '50%'; | |
| // Reset model to standard | |
| currentModel = 'standard'; | |
| modelValue.textContent = 'Standard'; | |
| standardModel.classList.add('bg-blue-600'); | |
| standardModel.classList.remove('bg-slate-700'); | |
| enhancedModel.classList.add('bg-slate-700'); | |
| enhancedModel.classList.remove('bg-blue-600'); | |
| // Disable process button | |
| processBtn.disabled = true; | |
| } | |
| // Batch resize functions | |
| function handleBatchFiles(files) { | |
| // Clear previous files | |
| batchFiles = []; | |
| // Convert FileList to array and filter images | |
| const fileArray = Array.from(files).filter(file => | |
| file.type.match('image.*') && file.size <= 10 * 1024 * 1024 | |
| ); | |
| // Limit to 20 files | |
| if (fileArray.length > 20) { | |
| alert('Maximum 20 images allowed. Only the first 20 will be processed.'); | |
| batchFiles = fileArray.slice(0, 20); | |
| } else { | |
| batchFiles = fileArray; | |
| } | |
| if (batchFiles.length === 0) { | |
| alert('No valid image files selected. Please select JPG or PNG files under 10MB.'); | |
| return; | |
| } | |
| // Show preview | |
| batchUploadContent.classList.add('hidden'); | |
| batchPreview.classList.remove('hidden'); | |
| batchPreview.innerHTML = ''; | |
| // Add each file to preview | |
| batchFiles.forEach((file, index) => { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const item = document.createElement('div'); | |
| item.className = 'batch-item'; | |
| item.innerHTML = ` | |
| <img src="${e.target.result}" class="batch-thumbnail"> | |
| <div class="batch-info"> | |
| <div class="font-medium truncate">${file.name}</div> | |
| <div class="text-xs text-slate-400">${formatFileSize(file.size)}</div> | |
| </div> | |
| <span class="batch-status status-pending">Pending</span> | |
| `; | |
| batchPreview.appendChild(item); | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| // Enable process button | |
| processBatchBtn.disabled = false; | |
| } | |
| function processBatchImages() { | |
| if (batchFiles.length === 0) { | |
| alert('Please select images first'); | |
| return; | |
| } | |
| // Get selected preset and quality | |
| const quality = parseInt(qualitySlider.value) / 100; | |
| // Show progress | |
| batchProgressContainer.classList.remove('hidden'); | |
| batchResults.classList.add('hidden'); | |
| // Reset results data | |
| batchResultsData = []; | |
| // Process each image | |
| let processed = 0; | |
| let success = 0; | |
| let errors = 0; | |
| // Calculate estimated time (1-3 seconds per image) | |
| const estimatedTime = batchFiles.length * (2 + Math.random()); | |
| let remainingTime = estimatedTime; | |
| const updateTime = setInterval(() => { | |
| remainingTime -= 0.1; | |
| if (remainingTime <= 0) remainingTime = 0; | |
| batchTimeRemaining.textContent = `Estimated time: ${remainingTime.toFixed(1)}s remaining`; | |
| }, 100); | |
| const processNextImage = () => { | |
| if (processed >= batchFiles.length) { | |
| clearInterval(updateTime); | |
| // Show results | |
| batchProgressContainer.classList.add('hidden'); | |
| batchResults.classList.remove('hidden'); | |
| processedCount.textContent = processed; | |
| successCount.textContent = success; | |
| errorCount.textContent = errors; | |
| return; | |
| } | |
| const file = batchFiles[processed]; | |
| const item = batchPreview.children[processed]; | |
| // Update progress | |
| processed++; | |
| const progress = (processed / batchFiles.length) * 100; | |
| batchProgressBar.style.width = `${progress}%`; | |
| batchProgressText.textContent = `${Math.round(progress)}% (${processed}/${batchFiles.length})`; | |
| // Process the image | |
| resizeImage(file, selectedPreset, quality).then(result => { | |
| if (result.status === 'success') { | |
| success++; | |
| item.querySelector('.batch-status').className = 'batch-status status-completed'; | |
| item.querySelector('.batch-status').textContent = 'Completed'; | |
| batchResultsData.push({ | |
| file: file, | |
| status: 'success', | |
| processedFile: result.blob, | |
| fileName: result.fileName | |
| }); | |
| } else { | |
| errors++; | |
| item.querySelector('.batch-status').className = 'batch-status status-error'; | |
| item.querySelector('.batch-status').textContent = 'Error'; | |
| batchResultsData.push({ | |
| file: file, | |
| status: 'error', | |
| error: result.error | |
| }); | |
| } | |
| // Process next image | |
| setTimeout(processNextImage, 300); | |
| }).catch(error => { | |
| errors++; | |
| item.querySelector('.batch-status').className = 'batch-status status-error'; | |
| item.querySelector('.batch-status').textContent = 'Error'; | |
| batchResultsData.push({ | |
| file: file, | |
| status: 'error', | |
| error: error.message | |
| }); | |
| // Process next image | |
| setTimeout(processNextImage, 300); | |
| }); | |
| }; | |
| // Start processing | |
| processNextImage(); | |
| } | |
| function resizeImage(file, preset, quality) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| const img = new Image(); | |
| img.onload = function() { | |
| try { | |
| // Determine target dimensions based on preset | |
| let targetWidth, targetHeight; | |
| if (preset === 'eviar') { | |
| // EVIAR: 3827 × 2551 pixels (32.4cm × 21.6cm @ 300dpi) | |
| targetWidth = 3827; | |
| targetHeight = 2551; | |
| } else { | |
| // SITE: 1000 × 667 pixels | |
| targetWidth = 1000; | |
| targetHeight = 667; | |
| } | |
| // Calculate new dimensions maintaining aspect ratio | |
| let newWidth, newHeight; | |
| const aspectRatio = img.width / img.height; | |
| if (img.width / img.height > targetWidth / targetHeight) { | |
| // Image is wider than target - scale to width | |
| newWidth = targetWidth; | |
| newHeight = targetWidth / aspectRatio; | |
| } else { | |
| // Image is taller than target - scale to height | |
| newHeight = targetHeight; | |
| newWidth = targetHeight * aspectRatio; | |
| } | |
| // Create canvas | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = newWidth; | |
| canvas.height = newHeight; | |
| const ctx = canvas.getContext('2d'); | |
| // Fill with white background (for transparent PNGs) | |
| ctx.fillStyle = 'white'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw image centered | |
| const offsetX = (targetWidth - newWidth) / 2; | |
| const offsetY = (targetHeight - newHeight) / 2; | |
| ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight); | |
| // Convert to blob | |
| canvas.toBlob(blob => { | |
| const fileName = file.name.replace(/\.[^/.]+$/, '') + | |
| (preset === 'eviar' ? '_eviar' : '_site') + | |
| (file.name.match(/\.[^/.]+$/) ? file.name.match(/\.[^/.]+$/)[0] : '.jpg'); | |
| resolve({ | |
| status: 'success', | |
| blob: blob, | |
| fileName: fileName | |
| }); | |
| }, 'image/jpeg', quality); | |
| } catch (error) { | |
| reject(new Error('Image processing failed')); | |
| } | |
| }; | |
| img.onerror = function() { | |
| reject(new Error('Failed to load image')); | |
| }; | |
| img.src = e.target.result; | |
| }; | |
| reader.onerror = function() { | |
| reject(new Error('Failed to read file')); | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| } | |
| function downloadBatchResults() { | |
| if (batchResultsData.length === 0 || batchResultsData.filter(item => item.status === 'success').length === 0) { | |
| alert('No successfully processed images to download'); | |
| return; | |
| } | |
| // Create a new JSZip instance | |
| const zip = new JSZip(); | |
| const successful = batchResultsData.filter(item => item.status === 'success'); | |
| // Add each successful image to the zip | |
| successful.forEach(item => { | |
| zip.file(item.fileName, item.processedFile); | |
| }); | |
| // Generate the zip file | |
| zip.generateAsync({ type: 'blob' }).then(content => { | |
| // Create download link | |
| const a = document.createElement('a'); | |
| const url = URL.createObjectURL(content); | |
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); | |
| const zipName = `batch-resize-${timestamp}.zip`; | |
| a.href = url; | |
| a.download = zipName; | |
| document.body.appendChild(a); | |
| a.click(); | |
| // Clean up | |
| setTimeout(() => { | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }, 0); | |
| }); | |
| } | |
| function resetBatchModal() { | |
| batchFileInput.value = ''; | |
| batchFiles = []; | |
| batchResultsData = []; | |
| batchUploadContent.classList.remove('hidden'); | |
| batchPreview.classList.add('hidden'); | |
| batchProgressContainer.classList.add('hidden'); | |
| batchResults.classList.add('hidden'); | |
| processBatchBtn.disabled = true; | |
| // Reset to default values | |
| qualitySlider.value = 85; | |
| qualityValue.textContent = '85%'; | |
| selectedPreset = 'eviar'; | |
| eviarPreset.classList.add('selected'); | |
| sitePreset.classList.remove('selected'); | |
| document.querySelector('input[name="preset"][value="eviar"]').checked = true; | |
| } | |
| // Helper functions | |
| function formatFileSize(bytes) { | |
| if (bytes < 1024) return bytes + ' bytes'; | |
| else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; | |
| else return (bytes / 1048576).toFixed(1) + ' MB'; | |
| } | |
| function getRandomDimensions() { | |
| const widths = [640, 800, 1024, 1280, 1920]; | |
| const heights = [480, 600, 768, 720, 1080]; | |
| const randomIndex = Math.floor(Math.random() * widths.length); | |
| const originalWidth = widths[randomIndex]; | |
| const originalHeight = heights[randomIndex]; | |
| const scale = parseInt(scaleSlider.value); | |
| const upscaledWidth = originalWidth * scale; | |
| const upscaledHeight = originalHeight * scale; | |
| return { | |
| original: `${originalWidth}×${originalHeight}`, | |
| upscaled: `${upscaledWidth}×${upscaledHeight}` | |
| }; | |
| } | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=zakkx2000/photo-imageperfect" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |