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> |