Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Online Image Compressor</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"> | |
<style> | |
.drop-zone { | |
border: 2px dashed #cbd5e0; | |
transition: all 0.3s ease; | |
} | |
.drop-zone.active { | |
border-color: #4299e1; | |
background-color: #ebf8ff; | |
} | |
.slider-thumb::-webkit-slider-thumb { | |
-webkit-appearance: none; | |
width: 20px; | |
height: 20px; | |
background: #4299e1; | |
border-radius: 50%; | |
cursor: pointer; | |
} | |
.slider-thumb::-moz-range-thumb { | |
width: 20px; | |
height: 20px; | |
background: #4299e1; | |
border-radius: 50%; | |
cursor: pointer; | |
} | |
.image-preview { | |
max-height: 300px; | |
object-fit: contain; | |
} | |
.file-info { | |
background-color: #f7fafc; | |
border-radius: 0.375rem; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50 min-h-screen flex flex-col"> | |
<!-- Header --> | |
<header class="bg-white shadow-sm py-4"> | |
<div class="container mx-auto px-4"> | |
<h1 class="text-2xl font-bold text-center text-blue-600">Online Image Compressor</h1> | |
</div> | |
</header> | |
<!-- Main Content --> | |
<main class="flex-grow container mx-auto px-4 py-8"> | |
<div class="max-w-4xl mx-auto bg-white rounded-lg shadow-md overflow-hidden"> | |
<!-- Upload Section --> | |
<div class="p-6 border-b"> | |
<div id="dropZone" class="drop-zone rounded-lg p-12 text-center cursor-pointer"> | |
<div class="flex flex-col items-center justify-center space-y-4"> | |
<i class="fas fa-cloud-upload-alt text-4xl text-blue-400"></i> | |
<h2 class="text-xl font-semibold text-gray-700">Drag & Drop or Click to Upload Images</h2> | |
<p class="text-gray-500">Supports JPEG, PNG, WEBP (Max 10MB)</p> | |
<input type="file" id="fileInput" class="hidden" accept="image/*" multiple> | |
<button id="selectFilesBtn" class="bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-6 rounded-lg transition duration-200"> | |
Select Files | |
</button> | |
</div> | |
</div> | |
<div id="fileList" class="mt-4 space-y-2 hidden"></div> | |
</div> | |
<!-- Settings Section --> | |
<div class="p-6 border-b"> | |
<h3 class="text-lg font-medium text-gray-800 mb-4">Compression Settings</h3> | |
<div class="space-y-6"> | |
<!-- Compression Level --> | |
<div> | |
<label for="compressionLevel" class="block text-sm font-medium text-gray-700 mb-1"> | |
Compression Level: <span id="compressionValue">70</span>% | |
</label> | |
<input type="range" id="compressionLevel" min="0" max="100" value="70" | |
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider-thumb"> | |
</div> | |
<!-- Maintain Aspect Ratio --> | |
<div class="flex items-center"> | |
<input type="checkbox" id="maintainAspect" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"> | |
<label for="maintainAspect" class="ml-2 block text-sm text-gray-700"> | |
Maintain Aspect Ratio | |
</label> | |
</div> | |
<!-- Custom Dimensions --> | |
<div class="grid grid-cols-2 gap-4"> | |
<div> | |
<label for="widthInput" class="block text-sm font-medium text-gray-700 mb-1">Width (px)</label> | |
<input type="number" id="widthInput" placeholder="Original" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> | |
</div> | |
<div> | |
<label for="heightInput" class="block text-sm font-medium text-gray-700 mb-1">Height (px)</label> | |
<input type="number" id="heightInput" placeholder="Original" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"> | |
</div> | |
</div> | |
<!-- Output Format --> | |
<div> | |
<label class="block text-sm font-medium text-gray-700 mb-1">Output Format</label> | |
<div class="flex space-x-4"> | |
<label class="inline-flex items-center"> | |
<input type="radio" name="outputFormat" value="jpeg" checked class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"> | |
<span class="ml-2 text-sm text-gray-700">JPEG</span> | |
</label> | |
<label class="inline-flex items-center"> | |
<input type="radio" name="outputFormat" value="png" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"> | |
<span class="ml-2 text-sm text-gray-700">PNG</span> | |
</label> | |
<label class="inline-flex items-center"> | |
<input type="radio" name="outputFormat" value="webp" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"> | |
<span class="ml-2 text-sm text-gray-700">WEBP</span> | |
</label> | |
</div> | |
</div> | |
</div> | |
<div class="mt-6 flex justify-center"> | |
<button id="compressBtn" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-8 rounded-lg transition duration-200 disabled:opacity-50 disabled:cursor-not-allowed" disabled> | |
Compress Images | |
</button> | |
</div> | |
</div> | |
<!-- Results Section --> | |
<div id="resultsSection" class="p-6 hidden"> | |
<h3 class="text-lg font-medium text-gray-800 mb-4">Results</h3> | |
<div id="resultsContainer" class="space-y-6"> | |
<!-- Results will be added here dynamically --> | |
</div> | |
<div class="mt-6 flex justify-center"> | |
<button id="downloadAllBtn" class="bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-8 rounded-lg transition duration-200 hidden"> | |
<i class="fas fa-download mr-2"></i> Download All | |
</button> | |
</div> | |
</div> | |
</div> | |
</main> | |
<!-- Footer --> | |
<footer class="bg-white border-t py-6"> | |
<div class="container mx-auto px-4"> | |
<div class="flex flex-col md:flex-row justify-between items-center"> | |
<div class="text-center md:text-left mb-4 md:mb-0"> | |
<p class="text-sm text-gray-500">All processing is done locally in your browser. No images are uploaded to the server.</p> | |
</div> | |
<div class="flex space-x-4"> | |
<a href="#" class="text-sm text-gray-600 hover:text-blue-600">Privacy Policy</a> | |
<a href="#" class="text-sm text-gray-600 hover:text-blue-600">Terms of Service</a> | |
<a href="#" class="text-sm text-gray-600 hover:text-blue-600">Contact Us</a> | |
</div> | |
</div> | |
</div> | |
</footer> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// DOM Elements | |
const dropZone = document.getElementById('dropZone'); | |
const fileInput = document.getElementById('fileInput'); | |
const selectFilesBtn = document.getElementById('selectFilesBtn'); | |
const fileList = document.getElementById('fileList'); | |
const compressionLevel = document.getElementById('compressionLevel'); | |
const compressionValue = document.getElementById('compressionValue'); | |
const maintainAspect = document.getElementById('maintainAspect'); | |
const widthInput = document.getElementById('widthInput'); | |
const heightInput = document.getElementById('heightInput'); | |
const compressBtn = document.getElementById('compressBtn'); | |
const resultsSection = document.getElementById('resultsSection'); | |
const resultsContainer = document.getElementById('resultsContainer'); | |
const downloadAllBtn = document.getElementById('downloadAllBtn'); | |
// Variables | |
let files = []; | |
let compressedFiles = []; | |
// Event Listeners | |
selectFilesBtn.addEventListener('click', () => fileInput.click()); | |
fileInput.addEventListener('change', handleFileSelect); | |
dropZone.addEventListener('dragover', handleDragOver); | |
dropZone.addEventListener('dragleave', handleDragLeave); | |
dropZone.addEventListener('drop', handleDrop); | |
compressionLevel.addEventListener('input', updateCompressionValue); | |
compressBtn.addEventListener('click', compressImages); | |
downloadAllBtn.addEventListener('click', downloadAllFiles); | |
// Maintain aspect ratio when width or height changes | |
let originalAspectRatio = null; | |
widthInput.addEventListener('change', () => { | |
if (maintainAspect.checked && originalAspectRatio && widthInput.value) { | |
heightInput.value = Math.round(widthInput.value / originalAspectRatio); | |
} | |
}); | |
heightInput.addEventListener('change', () => { | |
if (maintainAspect.checked && originalAspectRatio && heightInput.value) { | |
widthInput.value = Math.round(heightInput.value * originalAspectRatio); | |
} | |
}); | |
// Functions | |
function handleFileSelect(e) { | |
files = Array.from(e.target.files); | |
if (files.length > 0) { | |
displayFileList(); | |
} | |
} | |
function handleDragOver(e) { | |
e.preventDefault(); | |
dropZone.classList.add('active'); | |
} | |
function handleDragLeave() { | |
dropZone.classList.remove('active'); | |
} | |
function handleDrop(e) { | |
e.preventDefault(); | |
dropZone.classList.remove('active'); | |
files = Array.from(e.dataTransfer.files); | |
if (files.length > 0) { | |
displayFileList(); | |
} | |
} | |
function updateCompressionValue() { | |
compressionValue.textContent = compressionLevel.value; | |
} | |
function displayFileList() { | |
fileList.innerHTML = ''; | |
fileList.classList.remove('hidden'); | |
files.forEach((file, index) => { | |
const fileItem = document.createElement('div'); | |
fileItem.className = 'flex items-center p-3 bg-gray-50 rounded-lg'; | |
const fileIcon = document.createElement('i'); | |
fileIcon.className = 'fas fa-image text-blue-400 mr-3'; | |
const fileInfo = document.createElement('div'); | |
fileInfo.className = 'flex-grow'; | |
const fileName = document.createElement('div'); | |
fileName.className = 'text-sm font-medium text-gray-800 truncate'; | |
fileName.textContent = file.name; | |
const fileSize = document.createElement('div'); | |
fileSize.className = 'text-xs text-gray-500'; | |
fileSize.textContent = formatFileSize(file.size); | |
fileInfo.appendChild(fileName); | |
fileInfo.appendChild(fileSize); | |
const removeBtn = document.createElement('button'); | |
removeBtn.className = 'text-red-500 hover:text-red-700 ml-2'; | |
removeBtn.innerHTML = '<i class="fas fa-times"></i>'; | |
removeBtn.addEventListener('click', () => removeFile(index)); | |
fileItem.appendChild(fileIcon); | |
fileItem.appendChild(fileInfo); | |
fileItem.appendChild(removeBtn); | |
fileList.appendChild(fileItem); | |
}); | |
compressBtn.disabled = false; | |
} | |
function removeFile(index) { | |
files.splice(index, 1); | |
if (files.length > 0) { | |
displayFileList(); | |
} else { | |
fileList.classList.add('hidden'); | |
compressBtn.disabled = true; | |
} | |
} | |
function formatFileSize(bytes) { | |
if (bytes === 0) return '0 Bytes'; | |
const k = 1024; | |
const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
} | |
async function compressImages() { | |
if (files.length === 0) return; | |
compressBtn.disabled = true; | |
compressBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Compressing...'; | |
// Clear previous results | |
resultsContainer.innerHTML = ''; | |
compressedFiles = []; | |
// Get settings | |
const quality = compressionLevel.value / 100; | |
const outputFormat = document.querySelector('input[name="outputFormat"]:checked').value; | |
const width = widthInput.value ? parseInt(widthInput.value) : null; | |
const height = heightInput.value ? parseInt(heightInput.value) : null; | |
// Process each file | |
for (let i = 0; i < files.length; i++) { | |
const file = files[i]; | |
try { | |
const result = await processImage(file, quality, outputFormat, width, height); | |
compressedFiles.push(result); | |
displayResult(result, i); | |
} catch (error) { | |
console.error('Error processing image:', error); | |
displayError(file.name, error.message); | |
} | |
} | |
resultsSection.classList.remove('hidden'); | |
compressBtn.disabled = false; | |
compressBtn.textContent = 'Compress Images'; | |
if (compressedFiles.length > 1) { | |
downloadAllBtn.classList.remove('hidden'); | |
} else { | |
downloadAllBtn.classList.add('hidden'); | |
} | |
} | |
async function processImage(file, quality, format, targetWidth, targetHeight) { | |
return new Promise((resolve, reject) => { | |
const reader = new FileReader(); | |
reader.onload = function(e) { | |
const img = new Image(); | |
img.onload = function() { | |
// Calculate dimensions | |
let width = img.width; | |
let height = img.height; | |
// Store original aspect ratio | |
originalAspectRatio = width / height; | |
if (targetWidth || targetHeight) { | |
if (targetWidth && targetHeight) { | |
width = targetWidth; | |
height = targetHeight; | |
} else if (targetWidth) { | |
height = Math.round(targetWidth / (img.width / img.height)); | |
width = targetWidth; | |
} else if (targetHeight) { | |
width = Math.round(targetHeight * (img.width / img.height)); | |
height = targetHeight; | |
} | |
} | |
// Create canvas | |
const canvas = document.createElement('canvas'); | |
canvas.width = width; | |
canvas.height = height; | |
const ctx = canvas.getContext('2d'); | |
// Draw image on canvas | |
ctx.drawImage(img, 0, 0, width, height); | |
// Compress image | |
let mimeType; | |
switch (format) { | |
case 'jpeg': | |
mimeType = 'image/jpeg'; | |
break; | |
case 'png': | |
mimeType = 'image/png'; | |
break; | |
case 'webp': | |
mimeType = 'image/webp'; | |
break; | |
default: | |
mimeType = 'image/jpeg'; | |
} | |
canvas.toBlob((blob) => { | |
if (!blob) { | |
reject(new Error('Failed to compress image')); | |
return; | |
} | |
const compressedFile = new File([blob], `compressed_${file.name}`, { | |
type: mimeType, | |
lastModified: Date.now() | |
}); | |
resolve({ | |
original: file, | |
compressed: compressedFile, | |
originalImageData: e.target.result, | |
compressedImageData: URL.createObjectURL(blob), | |
width: width, | |
height: height | |
}); | |
}, mimeType, quality); | |
}; | |
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 displayResult(result, index) { | |
const resultItem = document.createElement('div'); | |
resultItem.className = 'border rounded-lg overflow-hidden'; | |
const header = document.createElement('div'); | |
header.className = 'bg-gray-50 px-4 py-2 border-b flex justify-between items-center'; | |
const fileName = document.createElement('div'); | |
fileName.className = 'text-sm font-medium text-gray-800 truncate'; | |
fileName.textContent = result.original.name; | |
const fileSize = document.createElement('div'); | |
fileSize.className = 'text-xs text-gray-500'; | |
fileSize.textContent = `${formatFileSize(result.original.size)} → ${formatFileSize(result.compressed.size)} (${Math.round((1 - result.compressed.size / result.original.size) * 100)}% smaller)`; | |
header.appendChild(fileName); | |
header.appendChild(fileSize); | |
const content = document.createElement('div'); | |
content.className = 'grid grid-cols-1 md:grid-cols-2 gap-4 p-4'; | |
// Original Image | |
const originalCol = document.createElement('div'); | |
originalCol.className = 'flex flex-col items-center'; | |
const originalLabel = document.createElement('div'); | |
originalLabel.className = 'text-sm font-medium text-gray-700 mb-2'; | |
originalLabel.textContent = 'Original'; | |
const originalImg = document.createElement('img'); | |
originalImg.src = result.originalImageData; | |
originalImg.className = 'image-preview max-w-full h-auto rounded border'; | |
originalImg.alt = 'Original image'; | |
const originalInfo = document.createElement('div'); | |
originalInfo.className = 'file-info text-xs text-gray-600 mt-2 px-3 py-2 text-center'; | |
originalInfo.textContent = `${result.original.width}×${result.original.height}px • ${formatFileSize(result.original.size)}`; | |
originalCol.appendChild(originalLabel); | |
originalCol.appendChild(originalImg); | |
originalCol.appendChild(originalInfo); | |
// Compressed Image | |
const compressedCol = document.createElement('div'); | |
compressedCol.className = 'flex flex-col items-center'; | |
const compressedLabel = document.createElement('div'); | |
compressedLabel.className = 'text-sm font-medium text-gray-700 mb-2'; | |
compressedLabel.textContent = 'Compressed'; | |
const compressedImg = document.createElement('img'); | |
compressedImg.src = result.compressedImageData; | |
compressedImg.className = 'image-preview max-w-full h-auto rounded border'; | |
compressedImg.alt = 'Compressed image'; | |
const compressedInfo = document.createElement('div'); | |
compressedInfo.className = 'file-info text-xs text-gray-600 mt-2 px-3 py-2 text-center'; | |
compressedInfo.textContent = `${result.width}×${result.height}px • ${formatFileSize(result.compressed.size)}`; | |
compressedCol.appendChild(compressedLabel); | |
compressedCol.appendChild(compressedImg); | |
compressedCol.appendChild(compressedInfo); | |
// Download Button | |
const downloadBtn = document.createElement('button'); | |
downloadBtn.className = 'bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium py-1 px-4 rounded transition duration-200 mt-2'; | |
downloadBtn.innerHTML = '<i class="fas fa-download mr-2"></i> Download'; | |
downloadBtn.addEventListener('click', () => downloadFile(result.compressed)); | |
compressedCol.appendChild(downloadBtn); | |
content.appendChild(originalCol); | |
content.appendChild(compressedCol); | |
resultItem.appendChild(header); | |
resultItem.appendChild(content); | |
resultsContainer.appendChild(resultItem); | |
} | |
function displayError(fileName, errorMessage) { | |
const errorItem = document.createElement('div'); | |
errorItem.className = 'border rounded-lg overflow-hidden bg-red-50'; | |
const header = document.createElement('div'); | |
header.className = 'bg-red-100 px-4 py-2 border-b border-red-200 flex justify-between items-center'; | |
const fileNameEl = document.createElement('div'); | |
fileNameEl.className = 'text-sm font-medium text-red-800 truncate'; | |
fileNameEl.textContent = fileName; | |
header.appendChild(fileNameEl); | |
const content = document.createElement('div'); | |
content.className = 'p-4 text-red-700 text-sm'; | |
content.textContent = `Error: ${errorMessage}`; | |
errorItem.appendChild(header); | |
errorItem.appendChild(content); | |
resultsContainer.appendChild(errorItem); | |
} | |
function downloadFile(file) { | |
const url = URL.createObjectURL(file); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = file.name; | |
document.body.appendChild(a); | |
a.click(); | |
document.body.removeChild(a); | |
URL.revokeObjectURL(url); | |
} | |
function downloadAllFiles() { | |
compressedFiles.forEach(file => { | |
downloadFile(file.compressed); | |
}); | |
} | |
}); | |
</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=drdata/image-compressor" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |