To quickly bypass chatwoot/chatwoot#12260
Created
August 21, 2025 11:53
-
-
Save rgon/80d0fb25a9b66d225a0c36fd93589a00 to your computer and use it in GitHub Desktop.
WebP/Other to JPEG Converter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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