这是一个图片批量转灰度的HTML工具,支持多张图片同时处理,并一键下载转换后的黑白图片。

<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">图片批量灰度工具 · 黑白转换</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
}
body {
background: #f2f4f8;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.card {
max-width: 800px;
width: 100%;
background: white;
border-radius: 32px;
box-shadow: 0 12px 30px rgba(0,0,0,0.08);
padding: 30px 28px;
transition: 0.2s;
}
h1 {
font-size: 26px;
font-weight: 620;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.sub {
color: #5f6b7a;
font-size: 15px;
margin-bottom: 24px;
border-left: 4px solid #1e293b;
padding-left: 16px;
background: #f8fafc;
border-radius: 0 8px 8px 0;
line-height: 1.5;
}
.drop-area {
border: 2px dashed #cdd5df;
background: #fafcff;
border-radius: 22px;
padding: 42px 20px;
text-align: center;
cursor: pointer;
transition: 0.2s;
position: relative;
}
.drop-area:hover {
border-color: #3b82f6;
background: #f0f6ff;
}
.drop-area.dragging {
border-color: #2563eb;
background: #eaf1fe;
box-shadow: 0 0 0 4px rgba(59,130,246,0.2);
}
.drop-icon {
font-size: 46px;
line-height: 1;
margin-bottom: 10px;
color: #4b5a6e;
}
.drop-text {
font-size: 16px;
font-weight: 500;
color: #1e293b;
}
.drop-hint {
font-size: 14px;
color: #6c7a8e;
margin-top: 8px;
}
.file-input {
display: none;
}
.img-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-top: 28px;
justify-content: center;
}
.img-card {
background: white;
border-radius: 20px;
box-shadow: 0 4px 14px rgba(0,0,0,0.04);
border: 1px solid #e9ecf2;
overflow: hidden;
flex: 1 1 150px;
max-width: 200px;
min-width: 130px;
transition: 0.1s;
}
.img-card figure {
margin: 0;
padding: 8px 8px 0 8px;
}
.img-card img {
width: 100%;
height: 130px;
object-fit: cover;
border-radius: 12px;
background: #f1f3f7;
display: block;
}
.img-card .info {
padding: 10px 8px 12px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: #1e293b;
background: #ffffff;
}
.img-card .filename {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 90px;
font-weight: 480;
}
.img-card .badge {
background: #eef2f6;
padding: 4px 10px;
border-radius: 30px;
font-size: 11px;
font-weight: 500;
color: #1a2634;
}
.action-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
margin-top: 28px;
gap: 12px;
}
.btn-group {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
background: white;
border: 1px solid #d0d7e2;
padding: 10px 22px;
border-radius: 60px;
font-size: 14px;
font-weight: 500;
color: #1a2634;
cursor: pointer;
transition: 0.15s;
background: #ffffff;
box-shadow: 0 1px 3px rgba(0,0,0,0.02);
}
.btn-primary {
background: #1e293b;
border: 1px solid #1e293b;
color: white;
box-shadow: 0 2px 8px rgba(30,41,59,0.15);
}
.btn-primary:hover {
background: #0f172b;
border-color: #0f172b;
box-shadow: 0 4px 12px rgba(30,41,59,0.25);
}
.btn-primary:active {
transform: scale(0.96);
}
.btn-outline {
border: 1px solid #d0d7e2;
background: white;
}
.btn-outline:hover {
background: #f4f7fc;
border-color: #b3bfcf;
}
.btn:disabled {
opacity: 0.5;
pointer-events: none;
filter: grayscale(0.3);
}
.btn-download {
background: #0f172b;
border: 1px solid #0f172b;
color: white;
}
.btn-download:hover {
background: #020617;
}
.status-msg {
margin-top: 18px;
padding: 12px 18px;
border-radius: 60px;
background: #f8fafc;
color: #334155;
font-size: 14px;
border: 1px solid #e5e9f0;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.status-msg .count {
background: #e4e9f2;
padding: 2px 12px;
border-radius: 50px;
font-weight: 500;
}
.status-msg .action-hint {
color: #3b82f6;
font-weight: 450;
}
.footer-note {
margin-top: 18px;
font-size: 13px;
color: #6b7b8f;
text-align: center;
border-top: 1px solid #eef2f6;
padding-top: 18px;
}
@media (max-width: 600px) {
.card { padding: 20px 16px; }
.img-card { flex: 1 1 120px; min-width: 100px; }
.img-card img { height: 100px; }
.btn { padding: 8px 16px; font-size: 13px; }
.action-bar { flex-direction: column; align-items: stretch; }
.btn-group { justify-content: stretch; }
.btn-group .btn { flex: 1; }
}
</style>
</head>
<body>
<div class="card">
<h1>
<span>⚫</span> 批量灰度转换
</h1>
<div class="sub">拖放或选择图片 — 转黑白 · 一键下载全部</div>
<!-- 拖放区 -->
<div class="drop-area" id="dropArea">
<div class="drop-icon">🖼️</div>
<div class="drop-text">点击选择图片 或 拖拽到此处</div>
<div class="drop-hint">支持 jpg, png, webp, bmp · 多选批量</div>
<input type="file" id="fileInput" class="file-input" multiple accept="image/png, image/jpeg, image/webp, image/bmp, image/tiff">
</div>
<!-- 缩略图网格 -->
<div class="img-grid" id="imgGrid"></div>
<!-- 操作栏 -->
<div class="action-bar">
<div class="btn-group">
<button class="btn btn-outline" id="addMoreBtn">➕ 添加更多</button>
<button class="btn btn-outline" id="clearBtn">🗑️ 清空</button>
</div>
<div class="btn-group">
<button class="btn btn-primary" id="convertBtn">⚡ 转为灰度</button>
<button class="btn btn-download" id="downloadBtn">⬇️ 下载全部</button>
</div>
</div>
<!-- 状态栏 -->
<div class="status-msg" id="statusMsg">
<span class="count" id="imgCount">0</span> 张图片
<span id="statusExtra">等待操作</span>
</div>
<div class="footer-note">
⚡ 点击「转为灰度」后图片预览会更新,下载将包含转换后的黑白版本
</div>
</div>
<script>
(function(){
// ----- 状态管理 -----
const files = []; // 存储原始 File 对象
const convertedBlobs = []; // 存储每个文件对应的灰度 blob (未转换时 null)
let currentImageUrls = []; // 用于预览的 objectURL (随时清理)
// DOM 元素
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('fileInput');
const imgGrid = document.getElementById('imgGrid');
const imgCountSpan = document.getElementById('imgCount');
const statusExtra = document.getElementById('statusExtra');
const addMoreBtn = document.getElementById('addMoreBtn');
const clearBtn = document.getElementById('clearBtn');
const convertBtn = document.getElementById('convertBtn');
const downloadBtn = document.getElementById('downloadBtn');
// ----- 辅助: 释放所有预览URL -----
function revokeAllUrls() {
currentImageUrls.forEach(url => {
if (url) URL.revokeObjectURL(url);
});
currentImageUrls = [];
}
// ----- 重新渲染网格 (根据files & convertedBlobs) -----
function renderGrid() {
// 清除旧预览
revokeAllUrls();
// 清空网格
imgGrid.innerHTML = '';
if (files.length === 0) {
imgCountSpan.textContent = '0';
statusExtra.textContent = '添加一些图片吧';
return;
}
// 构建新的预览
files.forEach((file, index) => {
// 判断是否有转换后的 blob
const hasConverted = convertedBlobs[index] !== null && convertedBlobs[index] !== undefined;
const blobToShow = hasConverted ? convertedBlobs[index] : file;
const url = URL.createObjectURL(blobToShow);
currentImageUrls.push(url);
const card = document.createElement('div');
card.className = 'img-card';
// 文件名截断
let displayName = file.name;
if (displayName.length > 12) {
const ext = displayName.lastIndexOf('.');
if (ext > 0) {
displayName = displayName.slice(0, 8) + '...' + displayName.slice(ext);
} else {
displayName = displayName.slice(0, 10) + '…';
}
}
const badgeText = hasConverted ? '黑白' : '原图';
card.innerHTML = `
<figure>
<img src="${url}" alt="${file.name}" loading="lazy">
</figure>
<div class="info">
<span class="filename" title="${file.name}">${displayName}</span>
<span class="badge">${badgeText}</span>
</div>
`;
imgGrid.appendChild(card);
});
// 更新计数 & 状态
imgCountSpan.textContent = files.length;
const convertedCount = convertedBlobs.filter(b => b !== null && b !== undefined).length;
if (convertedCount === 0) {
statusExtra.textContent = '未转换 · 点击「转为灰度」';
} else if (convertedCount === files.length) {
statusExtra.textContent = '全部已转为黑白 ✓';
} else {
statusExtra.textContent = `${convertedCount}/${files.length} 已转换`;
}
}
// ----- 添加文件 (过滤非图片, 去重可选) -----
function addFiles(newFiles) {
let added = 0;
for (const f of newFiles) {
// 简单类型过滤
if (!f.type.startsWith('image/')) continue;
// 可选去重 (可以根据 name+size 简单去重)
const duplicate = files.some(ex => ex.name === f.name && ex.size === f.size && ex.lastModified === f.lastModified);
if (duplicate) continue;
files.push(f);
convertedBlobs.push(null); // 未转换
added++;
}
if (added > 0) {
renderGrid();
statusExtra.textContent = `已添加 ${added} 张图片`;
} else {
statusExtra.textContent = '没有新图片被添加 (可能重复或格式不支持)';
}
}
// ----- 清空所有 -----
function clearAll() {
if (files.length === 0) return;
// 释放blob
convertedBlobs.forEach((blob, idx) => {
if (blob && blob !== files[idx]) {
// 如果是独立blob释放? 但都会被gc,不需要手动; 但为了安全不操作
}
});
// 清空数组
files.length = 0;
convertedBlobs.length = 0;
renderGrid();
statusExtra.textContent = '已清空';
}
// ----- 核心: 将单张图片转为灰度 Blob (canvas) -----
function convertToGrayscale(file) {
return new Promise((resolve, reject) => {
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
try {
// 创建 canvas 绘制
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('无法获取canvas上下文'));
return;
}
// 绘制原图
ctx.drawImage(img, 0, 0);
// 获取图像数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
// 灰度化 ( luminosity 加权 )
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// 更自然的灰度加权
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
data[i] = gray; // R
data[i + 1] = gray; // G
data[i + 2] = gray; // B
// alpha 不变
}
ctx.putImageData(imageData, 0, 0);
// 输出为 blob (保持原格式 png/jpg/webp, 默认png)
let mimeType = file.type;
// 对不支持的类型降级
if (!['image/jpeg', 'image/png', 'image/webp', 'image/bmp'].includes(mimeType)) {
mimeType = 'image/png';
}
// 如果原图是 jpeg 可以用较高压缩
const quality = (mimeType === 'image/jpeg') ? 0.92 : 0.95;
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('canvas toBlob 失败'));
}
}, mimeType, quality);
} catch (err) {
reject(err);
} finally {
URL.revokeObjectURL(url);
}
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('图片加载失败: ' + file.name));
};
img.src = url;
});
}
// ----- 批量转换 -----
async function batchConvert() {
if (files.length === 0) {
statusExtra.textContent = '没有图片需要转换';
return;
}
// 过滤已经转换的
const unprocessedIndices = [];
files.forEach((f, idx) => {
if (convertedBlobs[idx] === null || convertedBlobs[idx] === undefined) {
unprocessedIndices.push(idx);
}
});
if (unprocessedIndices.length === 0) {
statusExtra.textContent = '所有图片已经转为灰度';
return;
}
// 禁用按钮防止重复点击
convertBtn.disabled = true;
downloadBtn.disabled = true;
statusExtra.textContent = `正在转换 ${unprocessedIndices.length} 张...`;
let success = 0;
let fail = 0;
for (let i = 0; i < unprocessedIndices.length; i++) {
const idx = unprocessedIndices[i];
const file = files[idx];
try {
const grayBlob = await convertToGrayscale(file);
convertedBlobs[idx] = grayBlob;
success++;
} catch (err) {
console.warn('转换失败:', file.name, err);
fail++;
// 保留原始 (或者可选标记为失败? 保留null, 显示原图)
convertedBlobs[idx] = null; // 保持原图
}
// 更新状态 (每张)
statusExtra.textContent = `转换中 ${success+fail}/${unprocessedIndices.length} ...`;
// 每转换一张刷新一次网格? 为了流畅,最后统一刷新
}
// 重新渲染
renderGrid();
convertBtn.disabled = false;
downloadBtn.disabled = false;
if (fail === 0) {
statusExtra.textContent = `🎯 全部转换成功 (${success} 张)`;
} else {
statusExtra.textContent = `转换完成: ${success} 成功, ${fail} 失败 (保留原图)`;
}
}
// ----- 下载全部 (优先使用灰度版本) -----
function downloadAll() {
if (files.length === 0) {
statusExtra.textContent = '没有图片可下载';
return;
}
let downloaded = 0;
files.forEach((file, index) => {
// 决定使用哪个 blob: 如果有转换且不为 null 则用转换版
const blobToDownload = (convertedBlobs[index] !== null && convertedBlobs[index] !== undefined)
? convertedBlobs[index] : file;
// 生成文件名: 原文件名 + _gray 后缀 (如果已转换)
const isConverted = convertedBlobs[index] !== null && convertedBlobs[index] !== undefined;
const originalName = file.name;
const dotIndex = originalName.lastIndexOf('.');
const baseName = dotIndex > 0 ? originalName.slice(0, dotIndex) : originalName;
const ext = dotIndex > 0 ? originalName.slice(dotIndex) : '.png';
const suffix = isConverted ? '_bw' : '_original';
const newFileName = `${baseName}${suffix}${ext}`;
// 创建下载链接
const link = document.createElement('a');
link.href = URL.createObjectURL(blobToDownload);
link.download = newFileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 释放临时url (稍后批量释放)
setTimeout(() => URL.revokeObjectURL(link.href), 100);
downloaded++;
});
statusExtra.textContent = `⬇️ 已触发 ${downloaded} 个文件下载 (浏览器限制可能会询问)`;
}
// ----- 事件绑定 -----
// 1. 拖放
dropArea.addEventListener('dragover', (e) => {
e.preventDefault();
dropArea.classList.add('dragging');
});
dropArea.addEventListener('dragleave', () => {
dropArea.classList.remove('dragging');
});
dropArea.addEventListener('drop', (e) => {
e.preventDefault();
dropArea.classList.remove('dragging');
if (e.dataTransfer.files.length > 0) {
addFiles(e.dataTransfer.files);
}
});
// 2. 点击选择
dropArea.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
addFiles(e.target.files);
fileInput.value = ''; // 允许重复选择同一文件
}
});
// 3. 添加更多按钮
addMoreBtn.addEventListener('click', () => {
fileInput.click();
});
// 4. 清空按钮
clearBtn.addEventListener('click', clearAll);
// 5. 转换按钮
convertBtn.addEventListener('click', batchConvert);
// 6. 下载按钮
downloadBtn.addEventListener('click', downloadAll);
// 7. 额外: 防止浏览器默认拖拽打开链接
document.addEventListener('dragover', (e) => e.preventDefault());
document.addEventListener('drop', (e) => e.preventDefault());
// 初始化渲染
renderGrid();
statusExtra.textContent = '添加图片后可进行灰度转换';
})();
</script>
</body>
</html>
核心功能操作说明
您可以通过拖放或点击添加图片,然后一键完成批量灰度转换和下载,其核心流程如下:
- 添加图片:您可以通过拖拽图片到指定区域,或点击“添加更多”按钮,从本地选择多张图片。
- 执行转换:点击“转为灰度”按钮,工具会逐张将图片转换为黑白效果,并在缩略图上标记为“黑白”。
- 下载结果:所有图片转换完成后,点击“下载全部”按钮,即可将灰度版本(文件名带
_bw后缀)打包下载到本地。
优化建议: 您可以根据需要调整 convertToGrayscale 函数中的灰度加权系数(299 * r + 0.587 * g + 0.114 * b),以改变黑白转换的视觉效果。
标签: 黑白转换
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播