<!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色彩空间,整个过程在您的浏览器本地完成,不会上传图片。

- 上传图片:点击上传区域或直接拖拽 JPG、PNG、WebP 格式的图片到工具中。
- 执行优化:点击“批量 sRGB 优化”按钮,工具会使用 Canvas API 将每张图片重新编码并嵌入sRGB色彩配置文件。
- 查看与下载:优化后的图片会带有“✓ sRGB”标记,您可以点击“下载全部 (zip)”按钮,将所有优化后的图片打包下载。
优化建议: 您可以根据需要调整 optimizeToSRGB 函数中 quality 变量的值(0.85-0.95),以平衡图片质量和文件大小。
标签: 批量处理
版权声明:除非特别标注,否则均为本站原创文章,转载时请以链接形式注明文章出处。