my-homelab-configs/apps/demos-static/public/media-cruncher/media-cruncher.js

108 lines
4.5 KiB
JavaScript

const mediaInput = document.getElementById('media-input');
const dropZone = document.getElementById('drop-zone');
const results = document.getElementById('cruncher-results');
const outputFormat = document.getElementById('output-format');
const quality = document.getElementById('quality');
const qualityOutput = document.getElementById('quality-output');
const maxEdge = document.getElementById('max-edge');
const extensionByType = { 'image/webp': 'webp', 'image/jpeg': 'jpg', 'image/png': 'png' };
function formatBytes(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
}
function toBlob(canvas, type, imageQuality) {
if ('convertToBlob' in canvas) return canvas.convertToBlob({ type, quality: imageQuality });
return new Promise((resolve, reject) => canvas.toBlob((blob) => blob ? resolve(blob) : reject(new Error('Could not encode image')), type, imageQuality));
}
function createCanvas(width, height) {
if (typeof OffscreenCanvas !== 'undefined') return new OffscreenCanvas(width, height);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
}
function appendStatus(name, message) {
const row = document.createElement('div');
row.className = 'result-row';
const content = document.createElement('div');
const title = document.createElement('strong');
const detail = document.createElement('span');
title.textContent = name;
detail.textContent = message;
content.append(title, detail);
row.append(content);
results.append(row);
}
async function crunchImage(file) {
const bitmap = await createImageBitmap(file);
const originalWidth = bitmap.width;
const originalHeight = bitmap.height;
const maxSize = Math.max(320, Number(maxEdge.value) || 1920);
const scale = Math.min(1, maxSize / Math.max(originalWidth, originalHeight));
const width = Math.max(1, Math.round(originalWidth * scale));
const height = Math.max(1, Math.round(originalHeight * scale));
const canvas = createCanvas(width, height);
const context = canvas.getContext('2d', { alpha: outputFormat.value !== 'image/jpeg' });
context.drawImage(bitmap, 0, 0, width, height);
bitmap.close();
const type = outputFormat.value;
const imageQuality = type === 'image/png' ? undefined : Number(quality.value) / 100;
const blob = await toBlob(canvas, type, imageQuality);
const url = URL.createObjectURL(blob);
const saved = file.size - blob.size;
const ratio = file.size > 0 ? Math.round((saved / file.size) * 100) : 0;
const row = document.createElement('div');
const content = document.createElement('div');
const name = document.createElement('strong');
const dimensions = document.createElement('span');
const sizes = document.createElement('span');
const download = document.createElement('a');
row.className = 'result-row';
name.textContent = file.name;
dimensions.textContent = `${originalWidth}x${originalHeight} -> ${width}x${height}`;
sizes.textContent = `${formatBytes(file.size)} -> ${formatBytes(blob.size)} (${saved >= 0 ? ratio + '% smaller' : Math.abs(ratio) + '% larger'})`;
download.className = 'download-link';
download.download = `${file.name.replace(/\.[^.]+$/, '')}-crunched.${extensionByType[type] || 'bin'}`;
download.href = url;
download.textContent = 'Download';
content.append(name, dimensions, sizes);
row.append(content, download);
results.append(row);
}
async function handleFiles(files) {
results.innerHTML = '';
for (const file of files) {
if (file.type.startsWith('image/')) {
try {
await crunchImage(file);
} catch (error) {
appendStatus(file.name, error.message);
}
} else if (file.type.startsWith('video/')) {
appendStatus(file.name, 'Video accepted as a future Wasm codec target. Nothing was uploaded.');
} else {
appendStatus(file.name, 'Unsupported file type.');
}
}
}
quality.addEventListener('input', () => { qualityOutput.textContent = `${quality.value}%`; });
mediaInput.addEventListener('change', () => handleFiles(mediaInput.files));
['dragenter', 'dragover'].forEach((eventName) => dropZone.addEventListener(eventName, (event) => {
event.preventDefault();
dropZone.classList.add('is-dragging');
}));
['dragleave', 'drop'].forEach((eventName) => dropZone.addEventListener(eventName, (event) => {
event.preventDefault();
dropZone.classList.remove('is-dragging');
}));
dropZone.addEventListener('drop', (event) => handleFiles(event.dataTransfer.files));