HTML+CSS+JS實(shí)現(xiàn)簡單照片壓縮工具的示例詳解
用HTML開發(fā)一個(gè)瀏覽器端照片壓縮工具,支持上傳圖片、調(diào)整壓縮質(zhì)量、實(shí)時(shí)預(yù)覽效果并下載。
詳細(xì)需求如下:
1. 需要確定壓縮后的圖片尺寸和大小一定要比壓縮前小
2. 界面中可以直尺查看,壓縮前的照片和壓縮后的照片 ,如果沒有的話就顯示暫無
3. 可以選擇自定義選擇輸出格式
4. 壓縮前后都是base64格式座位 img 標(biāo)簽的src 值
5. 上傳圖片組件一定要方式重復(fù)上傳
6. 輸出格式如果選為質(zhì)量較高的png格式圖片的時(shí)候,壓縮后的大小一定要比壓縮前小
7. 最終壓縮后的圖片的尺寸和大小一定要小于壓縮前的
8. 哪怕是png(無損壓縮)也要保證壓縮后的大小一定小于壓縮前的,要強(qiáng)制壓縮
完整代碼如下:
<html lang="zh-CN"><head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>高效照片壓縮工具</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="external nofollow" rel="stylesheet">
<!-- 配置Tailwind自定義顏色和字體 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#10B981',
neutral: '#64748B',
dark: '#1E293B',
light: '#F8FAFC'
},
fontFamily: {
inter: ['Inter', 'system-ui', 'sans-serif'],
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.shadow-soft {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}
.transition-custom {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.scale-hover {
@apply hover:scale-105 transition-all duration-300;
}
}
</style>
</head>
<body class="font-inter bg-gray-50 text-dark min-h-screen flex flex-col">
<!-- 頂部導(dǎo)航 -->
<header class="bg-white shadow-sm py-4 px-6 md:px-12">
<div class="container mx-auto flex justify-between items-center">
<div class="flex items-center space-x-2">
<i class="fa fa-compress text-primary text-2xl"></i>
<h1 class="text-xl md:text-2xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
高效照片壓縮工具
</h1>
</div>
<div class="text-sm text-neutral hidden md:block">
瀏覽器端處理 · 保護(hù)隱私 · 免費(fèi)使用
</div>
</div>
</header>
<!-- 主要內(nèi)容區(qū) -->
<main class="flex-grow container mx-auto p-4 md:p-8 lg:p-12">
<!-- 上傳區(qū)域 -->
<section class="mb-10">
<div id="upload-container" class="bg-white rounded-xl shadow-soft p-6 md:p-8 border-2 border-dashed border-gray-200 hover:border-primary transition-custom">
<div class="flex flex-col items-center justify-center text-center">
<i class="fa fa-cloud-upload text-5xl text-primary mb-4"></i>
<h2 class="text-xl font-semibold mb-2">上傳圖片</h2>
<p class="text-neutral mb-6 max-w-md">支持JPG、PNG等格式,文件將在瀏覽器中處理,不會(huì)上傳到服務(wù)器</p>
<label for="file-upload" class="bg-primary hover:bg-primary/90 text-white font-medium py-3 px-8 rounded-lg cursor-pointer transition-custom scale-hover flex items-center">
<i class="fa fa-file-image-o mr-2"></i>選擇圖片
</label>
<input id="file-upload" type="file" accept="image/*" class="hidden">
<p id="file-name" class="mt-4 text-sm text-neutral hidden"></p>
<button id="reset-upload" class="mt-4 text-neutral hover:text-red-500 transition-custom hidden">
<i class="fa fa-times-circle mr-1"></i> 重新選擇
</button>
</div>
</div>
</section>
<!-- 壓縮設(shè)置區(qū)域 -->
<section id="compression-settings" class="mb-10 hidden">
<div class="bg-white rounded-xl shadow-soft p-6 md:p-8">
<h2 class="text-xl font-semibold mb-6 flex items-center">
<i class="fa fa-sliders text-primary mr-2"></i>壓縮設(shè)置
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- 質(zhì)量設(shè)置 -->
<div>
<label for="quality" class="block text-sm font-medium text-gray-700 mb-2">
壓縮質(zhì)量: <span id="quality-value">80</span>%
</label>
<input type="range" id="quality" min="1" max="100" value="80" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-primary">
<p class="text-xs text-neutral mt-1">數(shù)值越低,壓縮率越高,畫質(zhì)可能下降越明顯</p>
</div>
<!-- 輸出格式 -->
<div>
<label for="output-format" class="block text-sm font-medium text-gray-700 mb-2">
輸出格式
</label>
<select id="output-format" class="w-full p-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-custom">
<option value="jpeg">JPEG</option>
<option value="png">PNG</option>
<option value="webp">WebP (現(xiàn)代格式)</option>
</select>
<p class="text-xs text-neutral mt-1">選擇壓縮后的圖片格式</p>
</div>
</div>
<div class="mt-6 flex justify-center">
<button id="compress-btn" class="bg-secondary hover:bg-secondary/90 text-white font-medium py-3 px-8 rounded-lg transition-custom scale-hover flex items-center">
<i class="fa fa-cog mr-2"></i>開始?jí)嚎s
</button>
</div>
</div>
</section>
<!-- 預(yù)覽區(qū)域 -->
<section id="preview-section" class="mb-10 hidden">
<div class="bg-white rounded-xl shadow-soft p-6 md:p-8">
<h2 class="text-xl font-semibold mb-6 flex items-center">
<i class="fa fa-eye text-primary mr-2"></i>壓縮前后對比
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- 原圖預(yù)覽 -->
<div class="flex flex-col items-center">
<h3 class="text-lg font-medium mb-4">原圖</h3>
<div class="relative w-full max-w-md h-64 md:h-80 bg-gray-100 rounded-lg flex items-center justify-center overflow-hidden mb-4">
<img id="original-image" src="" alt="原始圖片預(yù)覽" class="max-w-full max-h-full object-contain">
<div id="original-placeholder" class="text-center p-4">
<i class="fa fa-image text-4xl text-gray-300 mb-2"></i>
<p class="text-gray-400">暫無圖片</p>
</div>
</div>
<div class="text-sm text-neutral w-full max-w-md">
<p><span class="font-medium">尺寸:</span> <span id="original-dimensions">--</span></p>
<p><span class="font-medium">大小:</span> <span id="original-size">--</span></p>
<p><span class="font-medium">格式:</span> <span id="original-format">--</span></p>
</div>
</div>
<!-- 壓縮后預(yù)覽 -->
<div class="flex flex-col items-center">
<h3 class="text-lg font-medium mb-4">壓縮后</h3>
<div class="relative w-full max-w-md h-64 md:h-80 bg-gray-100 rounded-lg flex items-center justify-center overflow-hidden mb-4">
<img id="compressed-image" src="" alt="壓縮后圖片預(yù)覽" class="max-w-full max-h-full object-contain">
<div id="compressed-placeholder" class="text-center p-4">
<i class="fa fa-image text-4xl text-gray-300 mb-2"></i>
<p class="text-gray-400">暫無圖片</p>
</div>
</div>
<div class="text-sm text-neutral w-full max-w-md">
<p><span class="font-medium">尺寸:</span> <span id="compressed-dimensions">--</span></p>
<p><span class="font-medium">大小:</span> <span id="compressed-size">--</span></p>
<p><span class="font-medium">格式:</span> <span id="compressed-format">--</span></p>
</div>
</div>
</div>
<div class="mt-8 flex justify-center">
<button id="download-btn" class="bg-primary hover:bg-primary/90 text-white font-medium py-3 px-8 rounded-lg transition-custom scale-hover flex items-center disabled:opacity-50 disabled:cursor-not-allowed" disabled="">
<i class="fa fa-download mr-2"></i>下載壓縮圖片
</button>
</div>
</div>
</section>
<!-- 提示信息區(qū)域 -->
<section id="info-section" class="bg-blue-50 border-l-4 border-primary p-4 rounded-r-lg mb-10 hidden">
<div class="flex">
<div class="flex-shrink-0">
<i class="fa fa-info-circle text-primary"></i>
</div>
<div class="ml-3">
<p class="text-sm text-blue-700" id="info-message">
提示信息將顯示在這里
</p>
</div>
</div>
</section>
</main>
<!-- 頁腳 -->
<footer class="bg-white border-t border-gray-200 py-6 px-6">
<div class="container mx-auto text-center text-sm text-neutral">
<p>高效照片壓縮工具 ? 2025 - 所有圖片均在您的瀏覽器中處理,不會(huì)上傳到任何服務(wù)器</p>
</div>
</footer>
<!-- 加載動(dòng)畫 -->
<div id="loading-overlay" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
<div class="bg-white p-6 rounded-lg shadow-lg flex items-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mr-4"></div>
<p class="font-medium">正在處理圖片...</p>
</div>
</div>
<script>
// 全局變量
let originalImageData = null;
let compressedImageData = null;
let isImageUploaded = false;
// DOM 元素
const fileUpload = document.getElementById('file-upload');
const fileName = document.getElementById('file-name');
const resetUpload = document.getElementById('reset-upload');
const uploadContainer = document.getElementById('upload-container');
const compressionSettings = document.getElementById('compression-settings');
const previewSection = document.getElementById('preview-section');
const infoSection = document.getElementById('info-section');
const infoMessage = document.getElementById('info-message');
const qualitySlider = document.getElementById('quality');
const qualityValue = document.getElementById('quality-value');
const outputFormat = document.getElementById('output-format');
const compressBtn = document.getElementById('compress-btn');
const downloadBtn = document.getElementById('download-btn');
const originalImage = document.getElementById('original-image');
const compressedImage = document.getElementById('compressed-image');
const originalPlaceholder = document.getElementById('original-placeholder');
const compressedPlaceholder = document.getElementById('compressed-placeholder');
const originalDimensions = document.getElementById('original-dimensions');
const originalSize = document.getElementById('original-size');
const originalFormat = document.getElementById('original-format');
const compressedDimensions = document.getElementById('compressed-dimensions');
const compressedSize = document.getElementById('compressed-size');
const compressedFormat = document.getElementById('compressed-format');
const loadingOverlay = document.getElementById('loading-overlay');
// 事件監(jiān)聽器
fileUpload.addEventListener('change', handleFileUpload);
resetUpload.addEventListener('click', resetUploadedFile);
qualitySlider.addEventListener('input', updateQualityValue);
compressBtn.addEventListener('click', compressImage);
downloadBtn.addEventListener('click', downloadCompressedImage);
// 顯示質(zhì)量值
function updateQualityValue() {
qualityValue.textContent = qualitySlider.value;
}
// 處理文件上傳
function handleFileUpload(e) {
const file = e.target.files[0];
if (!file) return;
// 檢查是否是圖片文件
if (!file.type.startsWith('image/')) {
showInfo('請上傳有效的圖片文件', 'error');
resetUploadedFile();
return;
}
// 防止重復(fù)上傳相同文件
if (isImageUploaded) {
showInfo('已上傳圖片,請先壓縮或重新選擇', 'warning');
return;
}
// 讀取文件并顯示
const reader = new FileReader();
reader.onload = function(event) {
const base64Data = event.target.result;
// 創(chuàng)建圖片對象獲取尺寸信息
const img = new Image();
img.onload = function() {
// 存儲(chǔ)原始圖片數(shù)據(jù)
originalImageData = {
base64: base64Data,
width: img.width,
height: img.height,
size: file.size,
format: file.type.split('/')[1]
};
// 更新UI
updateOriginalImageUI();
fileName.textContent = file.name;
fileName.classList.remove('hidden');
resetUpload.classList.remove('hidden');
compressionSettings.classList.remove('hidden');
previewSection.classList.remove('hidden');
uploadContainer.classList.remove('border-dashed', 'border-gray-200');
uploadContainer.classList.add('border-primary');
isImageUploaded = true;
showInfo('圖片上傳成功,請調(diào)整壓縮設(shè)置并點(diǎn)擊"開始?jí)嚎s"', 'info');
};
img.src = base64Data;
};
reader.readAsDataURL(file);
}
// 重置上傳
function resetUploadedFile() {
fileUpload.value = '';
fileName.classList.add('hidden');
resetUpload.classList.add('hidden');
uploadContainer.classList.add('border-dashed', 'border-gray-200');
uploadContainer.classList.remove('border-primary');
// 重置圖片數(shù)據(jù)
originalImageData = null;
compressedImageData = null;
isImageUploaded = false;
// 重置UI
resetOriginalImageUI();
resetCompressedImageUI();
compressionSettings.classList.add('hidden');
previewSection.classList.add('hidden');
infoSection.classList.add('hidden');
downloadBtn.disabled = true;
}
// 更新原始圖片UI
function updateOriginalImageUI() {
originalImage.src = originalImageData.base64;
originalImage.classList.remove('hidden');
originalPlaceholder.classList.add('hidden');
originalDimensions.textContent = `${originalImageData.width} × ${originalImageData.height} 像素`;
originalSize.textContent = formatFileSize(originalImageData.size);
originalFormat.textContent = originalImageData.format.toUpperCase();
}
// 重置原始圖片UI
function resetOriginalImageUI() {
originalImage.src = '';
originalImage.classList.add('hidden');
originalPlaceholder.classList.remove('hidden');
originalDimensions.textContent = '--';
originalSize.textContent = '--';
originalFormat.textContent = '--';
}
// 重置壓縮圖片UI
function resetCompressedImageUI() {
compressedImage.src = '';
compressedImage.classList.add('hidden');
compressedPlaceholder.classList.remove('hidden');
compressedDimensions.textContent = '--';
compressedSize.textContent = '--';
compressedFormat.textContent = '--';
}
// 顯示提示信息
function showInfo(message, type = 'info') {
infoMessage.textContent = message;
infoSection.classList.remove('hidden');
// 設(shè)置不同類型的樣式
infoSection.className = '';
if (type === 'info') {
infoSection.classList.add('bg-blue-50', 'border-l-4', 'border-primary', 'p-4', 'rounded-r-lg', 'mb-10');
infoMessage.classList.add('text-blue-700');
infoMessage.classList.remove('text-yellow-700', 'text-red-700');
} else if (type === 'warning') {
infoSection.classList.add('bg-yellow-50', 'border-l-4', 'border-yellow-400', 'p-4', 'rounded-r-lg', 'mb-10');
infoMessage.classList.add('text-yellow-700');
infoMessage.classList.remove('text-blue-700', 'text-red-700');
} else if (type === 'error') {
infoSection.classList.add('bg-red-50', 'border-l-4', 'border-red-400', 'p-4', 'rounded-r-lg', 'mb-10');
infoMessage.classList.add('text-red-700');
infoMessage.classList.remove('text-blue-700', 'text-yellow-700');
}
// 5秒后自動(dòng)隱藏
setTimeout(() => {
infoSection.classList.add('hidden');
}, 5000);
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
else if (bytes < 1048576) return (bytes / 1024).toFixed(2) + ' KB';
else return (bytes / 1048576).toFixed(2) + ' MB';
}
// 計(jì)算Base64數(shù)據(jù)大小
function getBase64FileSize(base64String) {
// Base64編碼的字符串大小計(jì)算: (長度 * 3) / 4 - 填充字符數(shù)
const padding = (base64String.endsWith('==') ? 2 : base64String.endsWith('=') ? 1 : 0);
const fileSizeInBytes = (base64String.length * 3 / 4) - padding;
return Math.round(fileSizeInBytes);
}
// 壓縮圖片
function compressImage() {
if (!originalImageData) return;
showLoading(true);
resetCompressedImageUI();
// 使用setTimeout避免UI阻塞
setTimeout(() => {
try {
const img = new Image();
img.onload = function() {
// 創(chuàng)建canvas元素
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 設(shè)置初始尺寸為原圖尺寸
let width = img.width;
let height = img.height;
// 獲取用戶設(shè)置
const quality = qualitySlider.value / 100;
const format = outputFormat.value;
// 首次壓縮嘗試
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
// 根據(jù)格式獲取Base64數(shù)據(jù)
let base64Data;
if (format === 'jpeg') {
base64Data = canvas.toDataURL('image/jpeg', quality);
} else if (format === 'webp') {
base64Data = canvas.toDataURL('image/webp', quality);
} else { // png
// PNG格式不支持quality參數(shù),我們需要通過其他方式壓縮
base64Data = canvas.toDataURL('image/png');
}
// 計(jì)算壓縮后大小
let compressedSize = getBase64FileSize(base64Data.split(',')[1]);
// 強(qiáng)制確保壓縮后大小小于原圖大小
let iterations = 0;
const maxIterations = 20; // 防止無限循環(huán)
// 如果壓縮后的大小仍然大于原圖,繼續(xù)壓縮
while (compressedSize >= originalImageData.size && iterations < maxIterations) {
iterations++;
// 每次迭代減小10%的尺寸
width = Math.round(width * 0.9);
height = Math.round(height * 0.9);
// 確保尺寸不會(huì)過小
if (width < 10 || height < 10) break;
// 重新繪制并壓縮
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
if (format === 'jpeg') {
base64Data = canvas.toDataURL('image/jpeg', Math.max(0.1, quality - (iterations * 0.1)));
} else if (format === 'webp') {
base64Data = canvas.toDataURL('image/webp', Math.max(0.1, quality - (iterations * 0.1)));
} else { // png
// 對于PNG,我們通過減小尺寸來壓縮
base64Data = canvas.toDataURL('image/png');
}
compressedSize = getBase64FileSize(base64Data.split(',')[1]);
}
// 存儲(chǔ)壓縮后的圖片數(shù)據(jù)
compressedImageData = {
base64: base64Data,
width: width,
height: height,
size: compressedSize,
format: format
};
// 更新UI
updateCompressedImageUI();
downloadBtn.disabled = false;
// 計(jì)算壓縮率
const rate = ((1 - compressedSize / originalImageData.size) * 100).toFixed(1);
showInfo(`圖片壓縮成功,壓縮率: ${rate}%`, 'info');
showLoading(false);
};
img.src = originalImageData.base64;
} catch (error) {
showInfo(`壓縮失敗: ${error.message}`, 'error');
showLoading(false);
}
}, 100);
}
// 更新壓縮圖片UI
function updateCompressedImageUI() {
if (!compressedImageData) return;
compressedImage.src = compressedImageData.base64;
compressedImage.classList.remove('hidden');
compressedPlaceholder.classList.add('hidden');
compressedDimensions.textContent = `${compressedImageData.width} × ${compressedImageData.height} 像素`;
compressedSize.textContent = formatFileSize(compressedImageData.size);
compressedFormat.textContent = compressedImageData.format.toUpperCase();
}
// 下載壓縮后的圖片
function downloadCompressedImage() {
if (!compressedImageData) return;
const link = document.createElement('a');
link.href = compressedImageData.base64;
// 生成文件名
const originalName = fileName.textContent.split('.').slice(0, -1).join('.');
link.download = `${originalName}_compressed.${compressedImageData.format}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showInfo('圖片已下載', 'info');
}
// 顯示/隱藏加載動(dòng)畫
function showLoading(show) {
if (show) {
loadingOverlay.classList.remove('hidden');
} else {
loadingOverlay.classList.add('hidden');
}
}
</script>
</body></html>結(jié)果如下:


到此這篇關(guān)于HTML+CSS+JS實(shí)現(xiàn)簡單照片壓縮工具的示例詳解的文章就介紹到這了,更多相關(guān)JS照片壓縮內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript金額運(yùn)算出現(xiàn)精度丟失問題的解決方案
在 JavaScript 中,浮點(diǎn)數(shù)運(yùn)算可能會(huì)產(chǎn)生精度丟失的問題,尤其在處理 金額計(jì)算 時(shí),本文為大家整理了一些常用方法,有需要的小伙伴可以根據(jù)需求進(jìn)行選擇2025-03-03
javascript實(shí)現(xiàn)手機(jī)震動(dòng)API代碼
一個(gè)新的API出來了。HTML5 (很快)將支持用戶設(shè)備振動(dòng)。這明顯是很有趣的事情,比如它可以用戶觸發(fā)提醒,提升游戲體驗(yàn),下面小編給大家整理javascript手機(jī)震動(dòng)api,需要的朋友可以參考下2015-08-08
解決layui動(dòng)態(tài)加載復(fù)選框無法選中的問題
今天小編就為大家分享一篇解決layui動(dòng)態(tài)加載復(fù)選框無法選中的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-09-09
淺談JavaScript中Date(日期對象),Math對象
這篇文章主要簡單介紹了JavaScript中Date(日期對象),Math對象的相關(guān)資料,需要的朋友可以參考下2015-02-02
JavaScript中for循環(huán)與forEach的區(qū)別詳解
最近在代碼審查中發(fā)現(xiàn),團(tuán)隊(duì)成員對數(shù)組遍歷方式的選擇存在不少困惑,有人堅(jiān)持使用傳統(tǒng)的for循環(huán),有人則偏愛forEach方法,雙方各執(zhí)一詞,今天,我們就來深入探討這個(gè)問題,幫助你在實(shí)際開發(fā)中做出更合理的選擇,需要的朋友可以參考下2025-04-04
JS基于對象的特性實(shí)現(xiàn)去除數(shù)組中重復(fù)項(xiàng)功能詳解
這篇文章主要介紹了JS基于對象的特性實(shí)現(xiàn)去除數(shù)組中重復(fù)項(xiàng)功能,結(jié)合實(shí)例形式較為詳細(xì)的分析了js基于key值唯一性實(shí)現(xiàn)數(shù)組去重的具體步驟與相關(guān)操作技巧,需要的朋友可以參考下2017-11-11

