自制压缩小程序
好久不更新了,今天来更一下😊
众所周知要压缩多张照片,并且大小限制得比较严格,用AI帮忙又容易给你搞砸了:
而其他网站的压缩工具要么不能严格给你压缩成你想要的大小,要么容易泄露隐私。于是我搞了一上午写了个网页小程序帮忙压缩,测试过效果还不错,源码分享给大家,复制源码,保存为html文件即可使用了,离线也能使用,很好地避免泄密的问题(^▽^),喜欢的话点点赞给个关注支持一下😎。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>高效批量图片压缩工具V1.6.5</title> <style> :root { --primary-color: #3498db; --error-color: #e74c3c; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 800px; margin: 0 auto; padding: 30px; background-color: #f5f7fa; } h1 { text-align: center; color: #2c3e50; margin-bottom: 30px; font-size: 2.2em; text-shadow: 2px 2px 4px rgba(0,0,0,0.1); } .input-group { margin: 25px 0; text-align: center; } .input-row { display: flex; align-items: center; justify-content: center; gap: 10px; margin: 15px 0; } input[type="file"] { padding: 10px; background: #fff; border: 2px solid var(--primary-color); border-radius: 6px; width: 300px; cursor: pointer; transition: all 0.3s; } #fileList { margin: 15px 0; padding: 10px; border: 2px dashed var(--primary-color); border-radius: 6px; min-height: 60px; } .file-item { padding: 8px; margin: 5px; background: #e8f4ff; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; } .progress-container { margin: 20px 0; } #globalProgress { height: 10px; background-color: #eee; border-radius: 5px; overflow: hidden; } #currentProgress { height: 10px; background-color: #2ecc71; width: 0%; transition: width 0.3s ease; } .status-text { color: #666; margin: 10px 0; font-size: 0.9em; text-align: center; } button { padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; transition: 0.3s; } button.primary { background-color: var(--primary-color); color: white; } button.cancel { background-color: var(--error-color); color: white; } @media (max-width: 600px) { body { padding: 15px; } input[type="file"] { width: 100%; } .input-row { flex-direction: column; } button { width: 100%; margin-top: 10px; } } </style> <script src="https://cdn.jsdelivr.net/npm/fflate@0.7.4/umd/index.min.js"></script> </head> <body> <h1> 高效批量图片压缩工具V1.6.5</h1> <div class="input-group"> <input type="file" id="imageInput" accept="image/*" multiple> </div> <div id="fileList"></div> <div class="input-group"> <div class="input-row"> <label for="targetSize">目标大小:</label> <input type="number" id="targetSize" step="0.1" min="0.1" value="1"> <select id="unitSelect"> <option value="MB">MB</option> <option value="KB">KB</option> </select> </div> </div> <div class="progress-container"> <div id="globalProgress"> <div id="currentProgress"></div> </div> <div class="status-text" id="status">准备就绪</div> </div> <div class="input-group"> <button class="primary" onclick="startCompression()">开始批量压缩</button> <button class="cancel" onclick="cancelCompression()">取消操作</button> </div> <h3> GAMES游戏出版社</h3> <h3> GAMES工作室</h3> <h5> lyh1190出品</h5> <script> // 配置常量 const CONCURRENCY = 3; const MAX_QUALITY = 0.9; const MIN_QUALITY = 0.3; const SIZE_STEP = 0.1; let files = []; let cancelRequested = false; let errorFiles = []; // 文件选择处理 document.getElementById('imageInput').addEventListener('change', async (e) => { files = Array.from(e.target.files); await updateFileListWithEstimation(); }); async function updateFileListWithEstimation() { const fileList = document.getElementById('fileList'); const items = await Promise.all(files.map(async (file, index) => { const estimatedSize = await estimateCompressedSize(file); return ` <div class="file-item"> <span> ${file.name}<br> <small>原大小: ${formatSize(file.size)} → 预估: ${estimatedSize}</small> </span> <button onclick="removeFile(${index})">×</button> </div> `; })); fileList.innerHTML = items.join(''); } async function estimateCompressedSize(file) { try { const img = await loadImage(file); const canvas = document.createElement('canvas'); const blob = await drawImageToBlob(img, canvas, 0.7, 0.8); return formatSize(blob.size); } catch { return '估算失败'; } } function removeFile(index) { files.splice(index, 1); updateFileListWithEstimation(); } async function startCompression() { if (!files.length) return alert('请选择图片文件'); resetState(); const startTime = Date.now(); const zip = {}; try { const targetSizeBytes = calculateTargetSize(); const chunks = chunkArray(files, CONCURRENCY); for (const [chunkIndex, chunk] of chunks.entries()) { if (cancelRequested) break; const results = await Promise.all(chunk.map(async (file) => { try { const blob = await optimizedCompression(file, targetSizeBytes); // 转换为Uint8Array const arrayBuffer = await blob.arrayBuffer(); return { file, uint8Array: new Uint8Array(arrayBuffer) }; } catch (error) { errorFiles.push(file.name); return null; } })); results.forEach((result, index) => { if (result) { const filename = `compressed_${result.file.name}`; zip[filename] = result.uint8Array; } updateProgress( (chunkIndex * CONCURRENCY + index + 1) / files.length ); }); updateStatus(chunkIndex, chunks.length, startTime); } if (!cancelRequested) { const zipped = fflate.zipSync(zip); // 创建正确类型的Blob saveAs(new Blob([zipped], { type: 'application/zip' }), "compressed_images.zip"); } } catch (error) { handleError(error); } finally { finalizeProcess(startTime); } } // 核心压缩逻辑(二分法优化) async function optimizedCompression(file, targetSize) { const img = await loadImage(file); const canvas = document.createElement('canvas'); let bestBlob = null; // 质量二分法优化 let low = MIN_QUALITY, high = MAX_QUALITY; for (let i = 0; i < 6; i++) { const mid = (low + high) / 2; const blob = await drawImageToBlob(img, canvas, mid, 1); if (blob.size <= targetSize) { bestBlob = blob; low = mid; } else { high = mid; } } // 尺寸调整作为后备方案 if (!bestBlob) { const scale = 0.8; const adjustedQuality = 0.7; bestBlob = await drawImageToBlob(img, canvas, adjustedQuality, scale); if (bestBlob.size > targetSize) { throw new Error('无法压缩到目标大小'); } } return bestBlob; } // 工具函数 function loadImage(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = reject; img.src = e.target.result; }; reader.readAsDataURL(file); }); } function drawImageToBlob(img, canvas, quality, scale) { canvas.width = img.width * scale; canvas.height = img.height * scale; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, canvas.width, canvas.height); return new Promise((resolve) => { canvas.toBlob( (blob) => resolve(blob), 'image/jpeg', quality ); }); } // 其他辅助函数 function calculateTargetSize() { const size = parseFloat(document.getElementById('targetSize').value); const unit = document.getElementById('unitSelect').value; return size * (unit === 'MB' ? 1048576 : 1024); } function chunkArray(arr, size) { return Array.from( { length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, (i + 1) * size) ); } function updateProgress(percentage) { document.getElementById('currentProgress').style.width = `${percentage * 100}%`; } function updateStatus(chunkIndex, totalChunks, startTime) { const elapsed = Math.round((Date.now() - startTime) / 1000); document.getElementById('status').textContent = `正在处理 ${chunkIndex + 1}/${totalChunks} 批次,已用时 ${elapsed} 秒`; } function resetState() { cancelRequested = false; errorFiles = []; document.getElementById('currentProgress').style.width = '0%'; document.getElementById('status').textContent = '初始化中...'; } function finalizeProcess(startTime) { const elapsed = Math.round((Date.now() - startTime) / 1000); let status = cancelRequested ? '操作已取消' : `处理完成,耗时 ${elapsed} 秒`; if (errorFiles.length) { status += `(${errorFiles.length} 个文件失败)`; alert(`压缩失败文件:\n${errorFiles.join('\n')}`); } document.getElementById('status').textContent = status; } function formatSize(bytes) { return bytes > 1048576 ? `${(bytes / 1048576).toFixed(1)} MB` : `${(bytes / 1024).toFixed(1)} KB`; } function cancelCompression() { cancelRequested = true; document.getElementById('status').textContent = '正在取消...'; } function saveAs(blob, filename) { const link = document.createElement('a'); link.style.display = 'none'; link.download = filename; link.href = URL.createObjectURL(blob); document.body.appendChild(link); link.click(); setTimeout(() => { document.body.removeChild(link); URL.revokeObjectURL(link.href); }, 100); } // 错误处理函数 function handleError(error) { console.error('处理过程中发生错误:', error); alert(`发生错误: ${error.message || error}`); } </script> </body> </html>本程序为我的团队“GAMES工作室”出品,喜欢的话请记得点赞哦👍
