108 lines
4.5 KiB
JavaScript
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));
|