image-compressor / index.html
drdata's picture
Add 2 files
2e80eb8 verified
<!DOCTYPE html>
<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>