<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片模糊和压缩</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.container {
display: flex;
gap: 40px;
flex-wrap: wrap;
}
.upload-section, .preview-section {
flex: 1;
min-width: 300px;
}
.control-panel {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.control-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="range"] {
width: 100%;
}
canvas {
max-width: 100%;
border: 1px solid #ddd;
border-radius: 4px;
}
.image-container {
margin-top: 20px;
}
.file-info {
background: #e9f7fe;
padding: 10px;
border-radius: 4px;
margin-bottom: 10px;
}
button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-right: 10px;
}
button:hover {
background: #0056b3;
}
.download-btn {
background: #28a745;
}
.download-btn:hover {
background: #1e7e34;
}
.processing {
position: relative;
}
.processing::after {
content: '处理中...';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.8);
color: white;
padding: 10px 20px;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>图片模糊和压缩处理</h1>
<div class="container">
<!-- 左侧:上传和控制面板 -->
<div class="upload-section">
<div class="control-panel">
<div class="control-group">
<label for="fileInput">选择图片:</label>
<input type="file" id="fileInput" accept="image/*">
</div>
<div class="control-group">
<label for="blurRange">模糊程度:<span id="blurValue">0</span>px</label>
<input type="range" id="blurRange" min="0" max="20" value="0" step="1">
</div>
<div class="control-group">
<label for="qualityRange">压缩质量:<span id="qualityValue">80</span>%</label>
<input type="range" id="qualityRange" min="10" max="100" value="80" step="5">
</div>
<div class="control-group">
<label for="maxWidth">最大宽度:<span id="maxWidthValue">1000</span>px</label>
<input type="range" id="maxWidth" min="100" max="2000" value="1000" step="50">
</div>
<button id="processBtn">处理图片</button>
<button id="resetBtn">重置</button>
<button id="downloadBtn" class="download-btn" disabled>下载图片</button>
</div>
<div id="originalInfo" class="file-info" style="display: none;">
<strong>原图信息:</strong>
<div>大小:<span id="originalSize">-</span></div>
<div>尺寸:<span id="originalDimensions">-</span></div>
</div>
<div class="image-container">
<h3>原始图片预览:</h3>
<canvas id="originalCanvas"></canvas>
</div>
</div>
<!-- 右侧:处理后预览 -->
<div class="preview-section">
<div id="processedInfo" class="file-info" style="display: none;">
<strong>处理后信息:</strong>
<div>大小:<span id="processedSize">-</span></div>
<div>尺寸:<span id="processedDimensions">-</span></div>
<div>压缩率:<span id="compressionRate">-</span></div>
</div>
<div class="image-container">
<h3>处理后图片:</h3>
<canvas id="processedCanvas"></canvas>
</div>
</div>
</div>
<script>
// 获取DOM元素
const fileInput = document.getElementById('fileInput');
const blurRange = document.getElementById('blurRange');
const qualityRange = document.getElementById('qualityRange');
const maxWidthInput = document.getElementById('maxWidth');
const processBtn = document.getElementById('processBtn');
const resetBtn = document.getElementById('resetBtn');
const downloadBtn = document.getElementById('downloadBtn');
const originalCanvas = document.getElementById('originalCanvas');
const processedCanvas = document.getElementById('processedCanvas');
const blurValue = document.getElementById('blurValue');
const qualityValue = document.getElementById('qualityValue');
const maxWidthValue = document.getElementById('maxWidthValue');
const originalInfo = document.getElementById('originalInfo');
const processedInfo = document.getElementById('processedInfo');
// 状态变量
let originalImage = null;
let processedBlob = null;
let originalSize = 0;
// 事件监听器
fileInput.addEventListener('change', handleFileSelect);
blurRange.addEventListener('input', () => blurValue.textContent = blurRange.value);
qualityRange.addEventListener('input', () => qualityValue.textContent = qualityRange.value);
maxWidthInput.addEventListener('input', () => maxWidthValue.textContent = maxWidthInput.value);
processBtn.addEventListener('click', processImage);
resetBtn.addEventListener('click', resetAll);
downloadBtn.addEventListener('click', downloadImage);
// 处理文件选择
function handleFileSelect(event) {
const file = event.target.files[0];
if (!file || !file.type.startsWith('image/')) {
alert('请选择有效的图片文件!');
return;
}
originalSize = file.size;
showOriginalInfo(file);
const reader = new FileReader();
reader.onload = function(e) {
originalImage = new Image();
originalImage.onload = function() {
drawOriginalImage();
processBtn.disabled = false;
};
originalImage.src = e.target.result;
};
reader.readAsDataURL(file);
}
// 显示原图信息
function showOriginalInfo(file) {
originalInfo.style.display = 'block';
document.getElementById('originalSize').textContent = formatFileSize(file.size);
}
// 绘制原始图片
function drawOriginalImage() {
const ctx = originalCanvas.getContext('2d');
// 设置canvas尺寸
originalCanvas.width = originalImage.width;
originalCanvas.height = originalImage.height;
// 清除并绘制
ctx.clearRect(0, 0, originalCanvas.width, originalCanvas.height);
ctx.drawImage(originalImage, 0, 0);
// 更新尺寸信息
document.getElementById('originalDimensions').textContent =
`${originalImage.width} × ${originalImage.height}`;
}
// 主处理函数
async function processImage() {
if (!originalImage) return;
// 显示处理中状态
processBtn.disabled = true;
processBtn.classList.add('processing');
try {
// 获取参数
const blurRadius = parseInt(blurRange.value);
const quality = parseInt(qualityRange.value) / 100;
const maxWidth = parseInt(maxWidthInput.value);
// 计算新尺寸
let newWidth = originalImage.width;
let newHeight = originalImage.height;
if (newWidth > maxWidth) {
newHeight = (maxWidth / newWidth) * newHeight;
newWidth = maxWidth;
}
// 设置处理后的canvas尺寸
processedCanvas.width = newWidth;
processedCanvas.height = newHeight;
const ctx = processedCanvas.getContext('2d');
// 1. 首先绘制缩放后的图片
ctx.drawImage(originalImage, 0, 0, newWidth, newHeight);
// 2. 应用模糊效果(如果模糊半径 > 0)
if (blurRadius > 0) {
await applyBlur(ctx, newWidth, newHeight, blurRadius);
}
// 3. 转换为Blob并压缩
processedCanvas.toBlob(
function(blob) {
processedBlob = blob;
// 显示处理后的信息
showProcessedInfo(blob, newWidth, newHeight);
// 启用下载按钮
downloadBtn.disabled = false;
// 移除处理中状态
processBtn.disabled = false;
processBtn.classList.remove('processing');
},
'image/jpeg', // 可以改为 'image/png' 如果需要透明背景
quality
);
} catch (error) {
console.error('处理图片时出错:', error);
alert('处理图片时出错,请重试!');
processBtn.disabled = false;
processBtn.classList.remove('processing');
}
}
// 应用模糊效果
async function applyBlur(ctx, width, height, radius) {
// 保存当前上下文
ctx.save();
// 方法1:使用CSS滤镜(简单但有限制)
// 这种方法只适用于现代浏览器
if (typeof ctx.filter !== 'undefined') {
ctx.filter = `blur(${radius}px)`;
ctx.clearRect(0, 0, width, height);
ctx.drawImage(originalImage, 0, 0, width, height);
ctx.filter = 'none';
}
// 方法2:使用堆栈模糊算法(兼容性更好)
else {
await stackBlur(ctx, width, height, radius);
}
ctx.restore();
}
// 堆栈模糊算法(纯JavaScript实现)
function stackBlur(ctx, width, height, radius) {
return new Promise(resolve => {
// 获取图像数据
const imageData = ctx.getImageData(0, 0, width, height);
const pixels = imageData.data;
// 堆栈模糊算法实现
const mul_table = [
512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512,
454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512,
482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456,
437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512,
497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328,
320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456,
446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335,
329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512
];
const shg_table = [
9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17,
17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19,
19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23
];
function processChannel(channelOffset) {
const vmin = Math.min(width, height);
const div = 2 * radius + 1;
const w4 = width << 2;
const divsum = (div + 1) >> 1;
const divsumSq = divsum * divsum;
const stack = new Array(div * 3);
let stackpointer, stackstart;
for(let x = 0; x < width; x++) {
let sum = 0;
let sumout = 0;
let sumin = 0;
let yi = x;
for(let r = -radius; r <= radius; r++) {
let p = yi + ((r < 0 ? 0 : r >= height ? height-1 : r) * width);
stack[r+radius] = pixels[(p << 2) + channelOffset];
sum += stack[r+radius] * (radius + 1 - Math.abs(r));
}
stackpointer = radius;
for(let y = 0; y < height; y++) {
pixels[(yi << 2) + channelOffset] = sum / divsum;
yi += width;
sum -= sumout;
stackstart = stackpointer + div - radius;
if(stackstart >= div) stackstart -= div;
sumout = stack[stackstart];
let p = x + ((y + radius + 1 < height ? y + radius + 1 : height - 1) * width);
sumin = pixels[(p << 2) + channelOffset];
sum += sumin;
stack[stackstart] = sumin;
if(++stackpointer >= div) stackpointer = 0;
}
}
for(let y = 0; y < height; y++) {
let sum = 0;
let sumout = 0;
let sumin = 0;
let yi = y * width;
for(let r = -radius; r <= radius; r++) {
let p = yi + (r < 0 ? 0 : r >= width ? width-1 : r);
stack[r+radius] = pixels[(p << 2) + channelOffset];
sum += stack[r+radius] * (radius + 1 - Math.abs(r));
}
stackpointer = radius;
for(let x = 0; x < width; x++) {
pixels[(yi << 2) + channelOffset] = sum / divsum;
yi++;
sum -= sumout;
stackstart = stackpointer + div - radius;
if(stackstart >= div) stackstart -= div;
sumout = stack[stackstart];
let p = x + radius + 1 < width ? x + radius + 1 : width - 1;
p += y * width;
sumin = pixels[(p << 2) + channelOffset];
sum += sumin;
stack[stackstart] = sumin;
if(++stackpointer >= div) stackpointer = 0;
}
}
}
// 处理RGB三个通道
processChannel(0); // R
processChannel(1); // G
processChannel(2); // B
// 将处理后的数据放回canvas
ctx.putImageData(imageData, 0, 0);
resolve();
});
}
// 显示处理后信息
function showProcessedInfo(blob, width, height) {
processedInfo.style.display = 'block';
const processedSize = blob.size;
const compressionRate = ((originalSize - processedSize) / originalSize * 100).toFixed(1);
document.getElementById('processedSize').textContent = formatFileSize(processedSize);
document.getElementById('processedDimensions').textContent = `${width} × ${height}`;
document.getElementById('compressionRate').textContent = `${compressionRate}%`;
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 下载处理后的图片
function downloadImage() {
if (!processedBlob) return;
const url = URL.createObjectURL(processedBlob);
const a = document.createElement('a');
a.href = url;
a.download = `processed_${Date.now()}.jpg`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
// 重置所有设置
function resetAll() {
// 重置控件
blurRange.value = 0;
qualityRange.value = 80;
maxWidthInput.value = 1000;
// 重置显示值
blurValue.textContent = '0';
qualityValue.textContent = '80';
maxWidthValue.textContent = '1000';
// 清除canvas
const ctx1 = originalCanvas.getContext('2d');
const ctx2 = processedCanvas.getContext('2d');
ctx1.clearRect(0, 0, originalCanvas.width, originalCanvas.height);
ctx2.clearRect(0, 0, processedCanvas.width, processedCanvas.height);
// 重置状态
originalCanvas.width = 1;
originalCanvas.height = 1;
processedCanvas.width = 1;
processedCanvas.height = 1;
// 隐藏信息面板
originalInfo.style.display = 'none';
processedInfo.style.display = 'none';
// 禁用按钮
processBtn.disabled = true;
downloadBtn.disabled = true;
// 清除文件输入
fileInput.value = '';
originalImage = null;
processedBlob = null;
}
// 页面加载时初始化
window.addEventListener('DOMContentLoaded', () => {
resetAll();
});
</script>
</body>
</html>
// 核心压缩方法:canvas.toBlob()
canvas.toBlob(
function(blob) {
// blob就是压缩后的图片数据
},
'image/jpeg', // 格式
0.8 // 质量(0-1)
);
// 尺寸压缩:通过调整canvas大小
function resizeImage(image, maxWidth) {
let width = image.width;
let height = image.height;
if (width > maxWidth) {
height = (maxWidth / width) * height;
width = maxWidth;
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(image, 0, 0, width, height);
}
方法1:使用Canvas滤镜(推荐,现代浏览器)
ctx.filter = `blur(${radius}px)`;
ctx.drawImage(image, 0, 0, width, height);
ctx.filter = 'none';
方法2:堆栈模糊算法(兼容性好)
// blur-worker.js
self.onmessage = function(e) {
const { imageData, width, height, radius } = e.data;
const pixels = imageData.data;
// 堆栈模糊算法实现...
self.postMessage({ imageData });
};
主线程使用:
async function applyBlurWithWorker(ctx, width, height, radius) {
const imageData = ctx.getImageData(0, 0, width, height);
const worker = new Worker('blur-worker.js');
worker.postMessage({ imageData, width, height, radius });
worker.onmessage = function(e) {
ctx.putImageData(e.data.imageData, 0, 0);
worker.terminate();
};
}
// 1. 基本使用
const processor = new ImageProcessor();
await processor.loadImage(file);
await processor.process({
blur: 5,
quality: 0.7,
maxWidth: 800
});
const blob = await processor.getBlob();
// 2. 批量处理
const files = [...]; // 多个文件
const promises = files.map(file =>
new ImageProcessor().processFile(file, options)
);
const results = await Promise.all(promises);
性能优化
兼容性处理
用户体验
安全考虑
这个实现完全在前端完成,不依赖任何后端服务,适合需要本地图片处理的场景。