高效批量图片压缩工具_原生js
作者:YXN-js 阅读量:8 发布日期:2025-06-11
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>高效批量图片压缩工具V2.0</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>???? 高效批量图片压缩工具V2.0</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>
<h5> 制作:徐</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>
YXN-js
2025-06-11