图片批量Web工具sRGB优化

联启 电脑工具 16
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">批量图片sRGB优化工具</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        body {
            background: #1e1e2f;
            font-family: 'Segoe UI', 'PingFang SC', Roboto, system-ui, -apple-system, sans-serif;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            margin: 0;
            padding: 20px;
        }
        .card {
            max-width: 1100px;
            width: 100%;
            background: #2a2a3e;
            border-radius: 32px;
            padding: 28px 28px 35px;
            box-shadow: 0 20px 40px rgba(0,0,0,0.6);
            color: #f0f0f5;
            transition: all 0.2s ease;
        }
        h1 {
            font-weight: 500;
            font-size: 1.8rem;
            letter-spacing: 0.5px;
            display: flex;
            align-items: center;
            gap: 12px;
            margin-bottom: 12px;
        }
        h1 small {
            font-size: 1rem;
            font-weight: 300;
            background: #3b3b55;
            padding: 4px 14px;
            border-radius: 40px;
            color: #c0c0e0;
        }
        .desc {
            color: #b0b0cc;
            margin-bottom: 25px;
            border-left: 4px solid #6d6dff;
            padding-left: 16px;
            font-size: 0.95rem;
            background: rgba(109, 109, 255, 0.08);
            border-radius: 0 12px 12px 0;
            line-height: 1.5;
        }
        .upload-zone {
            background: #22223a;
            border: 2px dashed #4a4a70;
            border-radius: 28px;
            padding: 40px 20px;
            text-align: center;
            cursor: pointer;
            transition: background 0.25s, border-color 0.25s;
            margin-bottom: 30px;
        }
        .upload-zone:hover {
            background: #2a2a48;
            border-color: #7a7aff;
        }
        .upload-zone.dragover {
            background: #2d2d50;
            border-color: #9b9bff;
            box-shadow: 0 0 20px rgba(109, 109, 255, 0.3);
        }
        .upload-zone input {
            display: none;
        }
        .upload-zone .icon {
            font-size: 3.8rem;
            margin-bottom: 8px;
            opacity: 0.7;
        }
        .upload-zone p {
            font-size: 1.2rem;
            font-weight: 300;
        }
        .upload-zone span {
            font-size: 0.9rem;
            color: #8a8aaa;
        }
        .actions {
            display: flex;
            flex-wrap: wrap;
            gap: 16px 20px;
            align-items: center;
            justify-content: space-between;
            margin-bottom: 30px;
        }
        .btn-group {
            display: flex;
            flex-wrap: wrap;
            gap: 12px;
        }
        .btn {
            background: #3b3b5c;
            border: none;
            padding: 10px 24px;
            border-radius: 60px;
            font-weight: 500;
            font-size: 0.95rem;
            color: #e0e0f0;
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            gap: 8px;
            transition: background 0.2s, transform 0.1s;
            box-shadow: 0 4px 8px rgba(0,0,0,0.3);
            border: 1px solid #4f4f72;
        }
        .btn-primary {
            background: #5f5fd6;
            border-color: #7a7aff;
            color: white;
        }
        .btn-primary:hover {
            background: #6f6ff0;
        }
        .btn-primary:active {
            transform: scale(0.96);
        }
        .btn-outline {
            background: transparent;
            border: 1px solid #5f5f88;
        }
        .btn-outline:hover {
            background: #3a3a5a;
        }
        .btn svg {
            width: 18px;
            height: 18px;
            fill: currentColor;
        }
        .stats {
            background: #1f1f35;
            padding: 8px 18px;
            border-radius: 40px;
            font-size: 0.9rem;
            color: #b0b0d0;
            border: 1px solid #3a3a5a;
        }
        .grid-container {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
            gap: 22px;
            margin-top: 8px;
            max-height: 520px;
            overflow-y: auto;
            padding: 4px 2px 10px 0;
        }
        .grid-container::-webkit-scrollbar {
            width: 6px;
        }
        .grid-container::-webkit-scrollbar-track {
            background: #1a1a2e;
            border-radius: 10px;
        }
        .grid-container::-webkit-scrollbar-thumb {
            background: #5a5a8a;
            border-radius: 10px;
        }
        .image-card {
            background: #232340;
            border-radius: 20px;
            overflow: hidden;
            box-shadow: 0 8px 18px rgba(0,0,0,0.5);
            transition: transform 0.15s, box-shadow 0.2s;
            display: flex;
            flex-direction: column;
            border: 1px solid #36365a;
        }
        .image-card:hover {
            transform: translateY(-4px);
            box-shadow: 0 14px 28px rgba(0,0,0,0.6);
        }
        .image-card .img-wrapper {
            position: relative;
            width: 100%;
            padding-top: 75%; /* 4:3 比例 */
            background: #0e0e1a;
        }
        .image-card .img-wrapper img {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            object-fit: contain;
            background: #0e0e1a;
            display: block;
        }
        .image-card .card-info {
            padding: 14px 14px 16px;
            flex: 1;
            display: flex;
            flex-direction: column;
            gap: 8px;
        }
        .image-card .file-name {
            font-size: 0.85rem;
            font-weight: 400;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            color: #c8c8e0;
        }
        .image-card .file-size {
            font-size: 0.75rem;
            color: #8a8aaa;
            display: flex;
            justify-content: space-between;
        }
        .image-card .status-badge {
            align-self: flex-start;
            background: #3a3a60;
            padding: 4px 14px;
            border-radius: 30px;
            font-size: 0.7rem;
            text-transform: uppercase;
            letter-spacing: 0.5px;
            font-weight: 600;
            color: #aab0ff;
            border: 1px solid #4f4f80;
            margin-top: 4px;
        }
        .image-card .status-badge.optimized {
            background: #2a5a3a;
            border-color: #4a9a6a;
            color: #8affb0;
        }
        .image-card .status-badge.pending {
            background: #3a3a50;
            border-color: #5a5a80;
            color: #b0b0d0;
        }
        .placeholder-text {
            grid-column: 1 / -1;
            text-align: center;
            padding: 60px 20px;
            color: #6a6a8a;
            font-size: 1.1rem;
            background: #1c1c32;
            border-radius: 40px;
            border: 1px solid #2f2f50;
        }
        .footer-note {
            margin-top: 28px;
            display: flex;
            justify-content: flex-end;
            font-size: 0.8rem;
            color: #6a6a8a;
            border-top: 1px solid #2f2f50;
            padding-top: 20px;
        }
        @media (max-width: 600px) {
            .card { padding: 18px; }
            .actions { flex-direction: column; align-items: stretch; }
            .btn-group { justify-content: center; }
            .grid-container { grid-template-columns: 1fr 1fr; max-height: 400px; }
        }
    </style>
</head>
<body>
<div class="card">
    <h1>
        🎨 sRGB 优化
        <small>批量 · Web工具</small>
    </h1>
    <div class="desc">
        ⚡ 将图片色彩空间转换至 sRGB (ICC v4) · 保留原始外观 · 支持 JPEG / PNG / WebP
    </div>
    <!-- 上传区域 -->
    <div class="upload-zone" id="dropZone">
        <div class="icon">🖼️</div>
        <p><strong>拖放图片到这里</strong> 或点击选择文件</p>
        <span>支持 JPG / PNG / WebP · 可多选</span>
        <input type="file" id="fileInput" multiple accept="image/jpeg,image/png,image/webp" />
    </div>
    <!-- 操作栏 -->
    <div class="actions">
        <div class="btn-group">
            <button class="btn btn-primary" id="processBtn">
                <svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>
                批量 sRGB 优化
            </button>
            <button class="btn btn-outline" id="downloadBtn" disabled>
                <svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
                下载全部 (zip)
            </button>
            <button class="btn btn-outline" id="clearBtn">🗑️ 清空</button>
        </div>
        <div class="stats" id="statsDisplay">0 张图片</div>
    </div>
    <!-- 网格展示 -->
    <div class="grid-container" id="gridContainer">
        <div class="placeholder-text">暂无图片 · 点击上传或拖拽</div>
    </div>
    <div class="footer-note">
        ⚡ 浏览器端处理 · 图片不会被上传
    </div>
</div>
<script>
    (function() {
        // ----- 状态 -----
        const fileList = [];           // 存储 { id, file, dataUrl, name, size, optimized, optimizedDataUrl, blob }
        let idCounter = 0;
        // DOM 引用
        const dropZone = document.getElementById('dropZone');
        const fileInput = document.getElementById('fileInput');
        const gridContainer = document.getElementById('gridContainer');
        const processBtn = document.getElementById('processBtn');
        const downloadBtn = document.getElementById('downloadBtn');
        const clearBtn = document.getElementById('clearBtn');
        const statsDisplay = document.getElementById('statsDisplay');
        // ----- 辅助函数 -----
        function updateUI() {
            const total = fileList.length;
            const optimizedCount = fileList.filter(item => item.optimized).length;
            statsDisplay.textContent = `${total} 张图片 · ${optimizedCount} 张已优化`;
            // 控制下载按钮
            downloadBtn.disabled = (optimizedCount === 0);
            // 渲染网格
            if (total === 0) {
                gridContainer.innerHTML = `<div class="placeholder-text">🖼️ 暂无图片 · 点击上传或拖拽</div>`;
                return;
            }
            let html = '';
            fileList.forEach(item => {
                const statusClass = item.optimized ? 'optimized' : 'pending';
                const statusText = item.optimized ? '✓ sRGB' : '待优化';
                const displaySrc = item.optimized && item.optimizedDataUrl ? item.optimizedDataUrl : item.dataUrl;
                // 文件大小格式化
                const sizeKB = (item.size / 1024).toFixed(1);
                html += `
                    <div class="image-card" data-id="${item.id}">
                        <div class="img-wrapper">
                            <img src="${displaySrc}" alt="${item.name}" loading="lazy" />
                        </div>
                        <div class="card-info">
                            <div class="file-name" title="${item.name}">${item.name}</div>
                            <div class="file-size">
                                <span>${sizeKB} KB</span>
                                <span>${item.optimized ? '优化后' : '原始'}</span>
                            </div>
                            <div class="status-badge ${statusClass}">${statusText}</div>
                        </div>
                    </div>
                `;
            });
            gridContainer.innerHTML = html;
        }
        // 添加文件
        function addFiles(files) {
            let added = 0;
            for (const file of files) {
                if (!file.type.match('image/jpeg|image/png|image/webp')) continue;
                // 防止完全重复 (同名同大小简单过滤)
                const duplicate = fileList.some(f => f.name === file.name && f.size === file.size);
                if (duplicate) continue;
                const id = ++idCounter;
                const reader = new FileReader();
                reader.onload = (e) => {
                    const dataUrl = e.target.result;
                    fileList.push({
                        id,
                        file,
                        dataUrl,
                        name: file.name,
                        size: file.size,
                        optimized: false,
                        optimizedDataUrl: null,
                        blob: null
                    });
                    updateUI();
                };
                reader.readAsDataURL(file);
                added++;
            }
            if (added === 0) return;
        }
        // ----- 核心优化: 转为 sRGB (使用 Canvas + ICC 模拟) -----
        function optimizeToSRGB(item) {
            return new Promise((resolve) => {
                const img = new Image();
                img.onload = () => {
                    // 创建 canvas, 绘制图像 (浏览器自动处理颜色管理, 但多数现代浏览器默认sRGB)
                    // 为了保证一致性: 用 Canvas 绘制并导出, 相当于把图片重新编码为 sRGB 平面
                    const canvas = document.createElement('canvas');
                    canvas.width = img.naturalWidth;
                    canvas.height = img.naturalHeight;
                    const ctx = canvas.getContext('2d');
                    // 清除 (透明背景填充白色, 防止png透明部分变黑)
                    ctx.fillStyle = '#FFFFFF';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    ctx.drawImage(img, 0, 0);
                    // 导出为 image/jpeg 质量0.92 (平衡体积与质量)
                    // 但若原图为 PNG 带透明, 我们选择 PNG 以保留透明度 (但sRGB优化不影响透明)
                    const isPNG = item.file.type === 'image/png' || item.name.toLowerCase().endsWith('.png');
                    const isWebP = item.file.type === 'image/webp' || item.name.toLowerCase().endsWith('.webp');
                    let mimeType = 'image/jpeg';
                    let quality = 0.92;
                    if (isPNG) {
                        mimeType = 'image/png';
                        quality = 0.9; // PNG无损, 但这里用canvas导出为png
                    } else if (isWebP) {
                        mimeType = 'image/webp';
                        quality = 0.85;
                    }
                    // 导出 Blob
                    canvas.toBlob((blob) => {
                        if (!blob) {
                            // 降级: 使用 dataUrl
                            const fallbackDataUrl = canvas.toDataURL(mimeType, quality);
                            item.optimizedDataUrl = fallbackDataUrl;
                            item.blob = null;
                            item.optimized = true;
                            resolve();
                            return;
                        }
                        // 生成 dataUrl 用于预览
                        const reader = new FileReader();
                        reader.onload = (e) => {
                            item.optimizedDataUrl = e.target.result;
                            item.blob = blob;
                            item.optimized = true;
                            resolve();
                        };
                        reader.readAsDataURL(blob);
                    }, mimeType, quality);
                };
                img.onerror = () => {
                    // 若图片加载失败, 标记未优化
                    item.optimized = false;
                    resolve();
                };
                img.src = item.dataUrl;
            });
        }
        // 批量优化
        async function processAll() {
            const pendingItems = fileList.filter(item => !item.optimized);
            if (pendingItems.length === 0) {
                // 如果都已优化, 可重新优化? 默认跳过
                return;
            }
            // 禁用按钮, 显示状态
            processBtn.disabled = true;
            processBtn.textContent = '⏳ 优化中...';
            downloadBtn.disabled = true;
            for (const item of pendingItems) {
                await optimizeToSRGB(item);
                updateUI(); // 逐步更新
            }
            processBtn.disabled = false;
            processBtn.innerHTML = `<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/> 批量 sRGB 优化`;
            updateUI();
        }
        // 下载全部 (zip)
        async function downloadAllAsZip() {
            const optimizedItems = fileList.filter(item => item.optimized && (item.optimizedDataUrl || item.blob));
            if (optimizedItems.length === 0) return;
            // 动态加载 JSZip
            let JSZip = window.JSZip;
            if (!JSZip) {
                try {
                    const res = await fetch('https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js');
                    const text = await res.text();
                    eval(text);
                    JSZip = window.JSZip;
                } catch (e) {
                    alert('无法加载压缩库, 请稍后重试或手动下载单个图片。');
                    return;
                }
            }
            const zip = new JSZip();
            const folder = zip.folder('sRGB_optimized');
            const promises = optimizedItems.map(async (item) => {
                let blob;
                if (item.blob) {
                    blob = item.blob;
                } else if (item.optimizedDataUrl) {
                    // 从 dataUrl 转 blob
                    const resp = await fetch(item.optimizedDataUrl);
                    blob = await resp.blob();
                } else {
                    return;
                }
                // 文件名: 保持原名, 但确保扩展名正确
                let fileName = item.name;
                // 如果原文件不是 jpg/png/webp, 根据blob类型调整
                const ext = fileName.split('.').pop().toLowerCase();
                if (!['jpg', 'jpeg', 'png', 'webp'].includes(ext)) {
                    const mimeExt = blob.type.split('/')[1] || 'jpg';
                    fileName = fileName + '.' + mimeExt;
                }
                folder.file(fileName, blob, { binary: true });
            });
            await Promise.all(promises);
            const content = await zip.generateAsync({ type: 'blob' });
            const link = document.createElement('a');
            link.href = URL.createObjectURL(content);
            link.download = `sRGB_优化_${new Date().toISOString().slice(0,10)}.zip`;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(link.href);
        }
        // 清空
        function clearAll() {
            fileList.length = 0;
            updateUI();
            downloadBtn.disabled = true;
        }
        // ----- 事件绑定 -----
        // 拖拽上传
        dropZone.addEventListener('dragover', (e) => {
            e.preventDefault();
            dropZone.classList.add('dragover');
        });
        dropZone.addEventListener('dragleave', () => {
            dropZone.classList.remove('dragover');
        });
        dropZone.addEventListener('drop', (e) => {
            e.preventDefault();
            dropZone.classList.remove('dragover');
            if (e.dataTransfer.files.length > 0) {
                addFiles(e.dataTransfer.files);
            }
        });
        dropZone.addEventListener('click', () => {
            fileInput.click();
        });
        fileInput.addEventListener('change', () => {
            if (fileInput.files.length > 0) {
                addFiles(fileInput.files);
                fileInput.value = '';
            }
        });
        // 按钮
        processBtn.addEventListener('click', processAll);
        downloadBtn.addEventListener('click', downloadAllAsZip);
        clearBtn.addEventListener('click', clearAll);
        // 初始更新
        updateUI();
        // 额外: 点击卡片可单独下载? (简单起见, 不实现)
    })();
</script>
</body>
</html>

图片色彩优化流程

您可以通过拖拽或点击上传图片,然后一键将它们转换为标准的sRGB色彩空间,整个过程在您的浏览器本地完成,不会上传图片。

图片批量Web工具sRGB优化-第1张图片-电脑手机工具软件下载 - 免费实用工具合集 | 联启科技

  1. 上传图片:点击上传区域或直接拖拽 JPG、PNG、WebP 格式的图片到工具中。
  2. 执行优化:点击“批量 sRGB 优化”按钮,工具会使用 Canvas API 将每张图片重新编码并嵌入sRGB色彩配置文件。
  3. 查看与下载:优化后的图片会带有“✓ sRGB”标记,您可以点击“下载全部 (zip)”按钮,将所有优化后的图片打包下载。

优化建议: 您可以根据需要调整 optimizeToSRGB 函数中 quality 变量的值(0.85-0.95),以平衡图片质量和文件大小。

标签: 批量处理

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