Skip to content

Instantly share code, notes, and snippets.

@rgon
Created August 21, 2025 11:53
Show Gist options
  • Select an option

  • Save rgon/80d0fb25a9b66d225a0c36fd93589a00 to your computer and use it in GitHub Desktop.

Select an option

Save rgon/80d0fb25a9b66d225a0c36fd93589a00 to your computer and use it in GitHub Desktop.
WebP/Other to JPEG Converter
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WEBP to JPEG Converter</title>
<!-- Tailwind CSS for styling -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Custom font and minor style adjustments */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* Custom scrollbar for the log container */
#log-container::-webkit-scrollbar {
width: 6px;
}
#log-container::-webkit-scrollbar-track {
background: #2d3748; /* gray-800 */
}
#log-container::-webkit-scrollbar-thumb {
background: #4a5568; /* gray-600 */
border-radius: 3px;
}
#log-container::-webkit-scrollbar-thumb:hover {
background: #718096; /* gray-500 */
}
</style>
</head>
<body class="bg-gray-900 text-gray-200 flex items-center justify-center min-h-screen p-4">
<div class="w-full max-w-md mx-auto text-center">
<!-- Main Title -->
<h1 class="text-3xl md:text-4xl font-bold mb-2 text-white">WEBP to JPEG</h1>
<p class="text-gray-400 mb-8">Drop an image file to instantly convert it.</p>
<!-- Hidden file input, triggered by the drop zone label -->
<input type="file" id="file-input" class="hidden" accept="image/*" />
<!-- Drop Zone Area -->
<label for="file-input" id="drop-zone" class="flex flex-col items-center justify-center w-full h-48 px-4 transition bg-gray-800 border-2 border-dashed rounded-xl border-gray-600 hover:border-sky-400 hover:bg-gray-700 cursor-pointer">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<svg class="w-10 h-10 mb-4 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
</svg>
<p class="mb-2 text-sm text-gray-400"><span class="font-semibold text-sky-400">Click to upload</span> or drag and drop</p>
<p class="text-xs text-gray-500">WEBP, PNG, GIF, or any other image</p>
</div>
</label>
<!-- Download Button Container (Initially Hidden) -->
<div id="download-container" class="mt-6 text-center hidden">
<button id="download-btn" class="bg-sky-500 text-white font-bold py-3 px-6 rounded-lg hover:bg-sky-600 transition duration-300 ease-in-out w-full">
Download
</button>
</div>
<!-- Message Log Container -->
<div id="log-container-wrapper" class="mt-8 text-left">
<h2 class="text-lg font-semibold text-gray-300 mb-2">Log</h2>
<div id="log-container" class="bg-gray-800 rounded-lg p-4 h-32 overflow-y-auto font-mono text-sm text-gray-400">
<!-- Log messages will be appended here -->
<p class="log-entry opacity-50">> Waiting for file...</p>
</div>
</div>
</div>
<script>
// --- DOM Element References ---
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
const logContainer = document.getElementById('log-container');
const downloadContainer = document.getElementById('download-container');
const downloadBtn = document.getElementById('download-btn');
// --- Event Listeners Setup ---
// Clicking the drop zone triggers the hidden file input
dropZone.addEventListener('click', () => fileInput.click());
// Handling file selection from the file dialog
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
}
});
// Drag and Drop listeners for the drop zone
dropZone.addEventListener('dragover', (e) => {
e.preventDefault(); // Necessary to allow drop
dropZone.classList.add('border-sky-400', 'bg-gray-700');
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.classList.remove('border-sky-400', 'bg-gray-700');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-sky-400', 'bg-gray-700');
if (e.dataTransfer.files.length > 0) {
handleFile(e.dataTransfer.files[0]);
}
});
// --- Core Application Logic ---
/**
* Handles the selected or dropped file.
* @param {File} file The file object to process.
*/
function handleFile(file) {
// Hide the download button when a new file is processed
downloadContainer.classList.add('hidden');
// Validate that the file is an image
if (!file.type.startsWith('image/')) {
logMessage(`Error: '${file.name}' is not an image file.`, 'error');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
// Once the image is loaded in memory, convert it
convertToJpeg(img, file.name);
};
img.onerror = () => {
logMessage(`Error: Could not load the image.`, 'error');
}
img.src = e.target.result;
};
reader.onerror = () => {
logMessage(`Error: Could not read the file.`, 'error');
}
reader.readAsDataURL(file);
}
/**
* Converts an Image object to JPEG and prepares the download button.
* @param {HTMLImageElement} image The loaded image element.
* @param {string} originalFilename The original name of the uploaded file.
*/
function convertToJpeg(image, originalFilename) {
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(image, 0, 0);
const newFilename = getNewFilename(originalFilename);
const dataUrl = canvas.toDataURL('image/jpeg', 0.92);
// 1. Set up the download button
downloadBtn.textContent = `Download ${newFilename}`;
downloadBtn.onclick = () => downloadImage(dataUrl, newFilename);
downloadContainer.classList.remove('hidden'); // Show the button
logMessage(`Ready to download ${newFilename}`, 'info');
// 2. Attempt to copy the image to the clipboard
// Clipboard API error: Error: Type 'image/jpeg' not supported for write
// canvas.toBlob((blob) => {
// if(blob) {
// copyImageToClipboard(blob, newFilename);
// } else {
// logMessage(`Error: Could not create blob for clipboard.`, 'error');
// }
// }, 'image/jpeg', 0.92);
}
/**
* Creates a temporary link and clicks it to download the image.
* @param {string} dataUrl The base64 data URL of the image.
* @param {string} filename The desired filename for the download.
*/
function downloadImage(dataUrl, filename) {
const link = document.createElement('a');
link.href = dataUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
logMessage(`Downloaded ${filename}`, 'success');
}
/**
* Uses the modern Clipboard API to copy the image blob.
* @param {Blob} blob The image blob to be copied.
* @param {string} filename The name of the file, used for logging.
*/
async function copyImageToClipboard(blob, filename) {
try {
await navigator.clipboard.write([
new ClipboardItem({ 'image/jpeg': blob })
]);
logMessage(`Copied ${filename} to clipboard`, 'success');
} catch (error) {
console.error('Clipboard API error:', error);
logMessage(`Clipboard copy failed. Browser may not support it.`, 'error');
}
}
// --- Utility Functions ---
/**
* Generates a new filename by replacing the extension with '.jpg'.
* @param {string} originalFilename The original filename.
* @returns {string} The new filename with a .jpg extension.
*/
function getNewFilename(originalFilename) {
const nameWithoutExtension = originalFilename.split('.').slice(0, -1).join('.');
return `${nameWithoutExtension || 'image'}.jpg`;
}
/**
* Appends a new message to the log container.
* @param {string} message The message to display.
* @param {'success'|'error'|'info'} type The type of message for styling.
*/
function logMessage(message, type = 'info') {
const initialMessage = logContainer.querySelector('.opacity-50');
if (initialMessage) {
initialMessage.remove();
}
const logEntry = document.createElement('p');
logEntry.textContent = `> ${message}`;
switch (type) {
case 'success':
logEntry.className = 'text-green-400';
break;
case 'error':
logEntry.className = 'text-red-400';
break;
default:
logEntry.className = 'text-gray-400';
}
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment