图片批量灰度工具黑白转换

联启 电脑工具 14

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

图片批量灰度工具黑白转换-第1张图片-电脑手机工具软件下载 - 免费实用工具合集 | 联启科技

<!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>

核心功能操作说明

您可以通过拖放或点击添加图片,然后一键完成批量灰度转换和下载,其核心流程如下:

  1. 添加图片:您可以通过拖拽图片到指定区域,或点击“添加更多”按钮,从本地选择多张图片。
  2. 执行转换:点击“转为灰度”按钮,工具会逐张将图片转换为黑白效果,并在缩略图上标记为“黑白”。
  3. 下载结果:所有图片转换完成后,点击“下载全部”按钮,即可将灰度版本(文件名带 _bw 后缀)打包下载到本地。

优化建议: 您可以根据需要调整 convertToGrayscale 函数中的灰度加权系数(299 * r + 0.587 * g + 0.114 * b),以改变黑白转换的视觉效果。

标签: 黑白转换

抱歉,评论功能暂时关闭!