图片批量伽马工具中间调调整

联启 电脑工具 17
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">图片批量伽马工具 · 中间调调整</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
            background: #1e1e2f;
            color: #e0e0e0;
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: flex-start;
            padding: 1.5rem;
        }
        .app {
            max-width: 1200px;
            width: 100%;
            background: #2a2a3d;
            border-radius: 24px;
            padding: 2rem;
            box-shadow: 0 15px 40px rgba(0,0,0,0.5);
        }
        h1 {
            font-weight: 500;
            font-size: 1.8rem;
            letter-spacing: 0.5px;
            display: flex;
            align-items: center;
            gap: 10px;
            margin-bottom: 0.25rem;
        }
        h1 small {
            font-size: 0.9rem;
            opacity: 0.7;
            font-weight: 300;
        }
        .subhead {
            margin-bottom: 2rem;
            opacity: 0.65;
            border-left: 3px solid #ffb347;
            padding-left: 15px;
            font-size: 0.95rem;
        }
        .main-controls {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: 1.5rem;
            background: #1f1f2e;
            padding: 1.2rem 1.8rem;
            border-radius: 60px;
            margin-bottom: 2rem;
            justify-content: space-between;
        }
        .gamma-slider-area {
            display: flex;
            align-items: center;
            gap: 1rem;
            flex-wrap: wrap;
        }
        .gamma-slider-area label {
            font-weight: 400;
            color: #bbb;
        }
        input[type="range"] {
            width: 200px;
            height: 6px;
            -webkit-appearance: none;
            background: #444;
            border-radius: 10px;
            outline: none;
        }
        input[type="range"]::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 18px;
            height: 18px;
            background: #ffb347;
            border-radius: 50%;
            cursor: pointer;
            box-shadow: 0 0 12px rgba(255, 180, 70, 0.4);
            transition: 0.1s;
        }
        input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.1); }
        .gamma-value {
            min-width: 42px;
            font-weight: 600;
            color: #ffb347;
            background: #2a2a3d;
            padding: 0.2rem 0.7rem;
            border-radius: 30px;
            text-align: center;
        }
        .file-actions {
            display: flex;
            align-items: center;
            gap: 0.8rem;
            flex-wrap: wrap;
        }
        .btn {
            background: #3d3d5c;
            border: none;
            color: #eee;
            padding: 0.55rem 1.4rem;
            border-radius: 40px;
            font-size: 0.9rem;
            font-weight: 500;
            cursor: pointer;
            transition: 0.2s;
            display: inline-flex;
            align-items: center;
            gap: 6px;
            white-space: nowrap;
            border: 1px solid #4f4f6b;
        }
        .btn-primary {
            background: #ffb347;
            color: #1e1e2f;
            border: none;
            font-weight: 600;
        }
        .btn-primary:hover { background: #ffa726; box-shadow: 0 4px 12px rgba(255, 180, 70, 0.3); }
        .btn-outline {
            background: transparent;
            border: 1px solid #5f5f7a;
        }
        .btn-outline:hover { background: #3a3a52; }
        .btn:active { transform: scale(0.96); }
        .stats {
            font-size: 0.85rem;
            opacity: 0.7;
            padding: 0 0.2rem;
        }
        .grid-container {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
            gap: 1.2rem;
            margin-top: 1.8rem;
            min-height: 120px;
        }
        .card {
            background: #23233a;
            border-radius: 18px;
            overflow: hidden;
            box-shadow: 0 4px 10px rgba(0,0,0,0.3);
            transition: transform 0.1s ease;
            display: flex;
            flex-direction: column;
            position: relative;
        }
        .card:hover { transform: translateY(-3px); }
        .card-img-wrapper {
            width: 100%;
            aspect-ratio: 1 / 1;
            background: #18182a;
            display: flex;
            align-items: center;
            justify-content: center;
            overflow: hidden;
        }
        .card-img-wrapper img {
            width: 100%;
            height: 100%;
            object-fit: cover;
            display: block;
            transition: 0.15s;
        }
        .card-body {
            padding: 0.5rem 0.7rem 0.7rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
            background: #1f1f32;
        }
        .card-body .filename {
            font-size: 0.75rem;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            max-width: 110px;
            opacity: 0.7;
        }
        .card-body .remove-btn {
            background: none;
            border: none;
            color: #ff6b6b;
            font-size: 1.1rem;
            cursor: pointer;
            padding: 0 4px;
            line-height: 1;
            opacity: 0.6;
            transition: 0.15s;
        }
        .card-body .remove-btn:hover { opacity: 1; transform: scale(1.1); }
        .empty-message {
            grid-column: 1 / -1;
            text-align: center;
            padding: 3rem 1rem;
            opacity: 0.5;
            font-style: italic;
            background: #1f1f30;
            border-radius: 40px;
            border: 1px dashed #444;
        }
        .batch-download-bar {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            justify-content: space-between;
            margin-top: 1.8rem;
            padding-top: 1.2rem;
            border-top: 1px solid #373752;
        }
        .progress-indicator {
            font-size: 0.9rem;
            opacity: 0.8;
        }
        @media (max-width: 640px) {
            .app { padding: 1rem; }
            .main-controls { border-radius: 30px; flex-direction: column; align-items: stretch; gap: 1rem; }
            .gamma-slider-area { justify-content: center; }
            input[type="range"] { width: 140px; }
            .grid-container { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
        }
    </style>
</head>
<body>
<div class="app">
    <h1>⚡ 伽马工具 <small>中间调 · 批量</small></h1>
    <div class="subhead">调整 Gamma 值(中间调对比度),实时预览,批量导出</div>
    <!-- 控制栏 -->
    <div class="main-controls">
        <div class="gamma-slider-area">
            <label for="gammaRange">Gamma</label>
            <input type="range" id="gammaRange" min="0.10" max="3.00" step="0.01" value="1.00">
            <span class="gamma-value" id="gammaDisplay">1.00</span>
        </div>
        <div class="file-actions">
            <input type="file" id="fileInput" multiple accept="image/png,image/jpeg,image/webp,image/bmp,image/gif,image/tiff" hidden>
            <button class="btn btn-primary" id="addBtn">📁 添加图片</button>
            <button class="btn btn-outline" id="clearBtn">🗑️ 清空</button>
            <span class="stats" id="imageCount">0 张</span>
        </div>
    </div>
    <!-- 图片网格 -->
    <div class="grid-container" id="gridContainer">
        <div class="empty-message">点击「添加图片」上传,中间调实时调整</div>
    </div>
    <!-- 批量导出 -->
    <div class="batch-download-bar">
        <div class="progress-indicator" id="progressInfo">就绪</div>
        <button class="btn btn-primary" id="downloadAllBtn">📥 批量导出 (ZIP)</button>
    </div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script>
(function() {
    'use strict';
    // DOM
    const fileInput = document.getElementById('fileInput');
    const addBtn = document.getElementById('addBtn');
    const clearBtn = document.getElementById('clearBtn');
    const gammaRange = document.getElementById('gammaRange');
    const gammaDisplay = document.getElementById('gammaDisplay');
    const grid = document.getElementById('gridContainer');
    const imageCountSpan = document.getElementById('imageCount');
    const progressInfo = document.getElementById('progressInfo');
    const downloadAllBtn = document.getElementById('downloadAllBtn');
    // 状态:存储图片数据
    let imageRecords = [];         // { id, fileName, originalDataUrl, adjustedDataUrl? (canvas生成) }
    let nextId = 0;
    // 当前 gamma (浮点数)
    let currentGamma = 1.0;
    // 标志:正在处理 (用于批量导出)
    let isProcessing = false;
    // ========== 工具函数 ==========
    function updateGammaDisplay() {
        gammaDisplay.textContent = currentGamma.toFixed(2);
    }
    // 更新统计信息
    function updateStats() {
        const len = imageRecords.length;
        imageCountSpan.textContent = `${len} 张`;
    }
    // 显示空状态
    function showEmptyIfNeeded() {
        const empty = grid.querySelector('.empty-message');
        if (imageRecords.length === 0) {
            if (!empty) {
                const msg = document.createElement('div');
                msg.className = 'empty-message';
                msg.textContent = '暂无图片 · 点击「添加图片」开始';
                grid.appendChild(msg);
            }
        } else {
            const emptyMsg = grid.querySelector('.empty-message');
            if (emptyMsg) emptyMsg.remove();
        }
    }
    // ========== 核心:渲染所有卡片 (基于当前 gamma 重新生成 adjusted) ==========
    function renderAllCards() {
        // 清除所有卡片 (保留 .empty-message 由后面逻辑处理)
        const cards = grid.querySelectorAll('.card');
        cards.forEach(c => c.remove());
        if (imageRecords.length === 0) {
            showEmptyIfNeeded();
            updateStats();
            return;
        }
        // 遍历记录,为每个创建卡片
        imageRecords.forEach((record, index) => {
            const card = document.createElement('div');
            card.className = 'card';
            card.dataset.id = record.id;
            // 图片容器
            const wrapper = document.createElement('div');
            wrapper.className = 'card-img-wrapper';
            const img = document.createElement('img');
            // 先显示原始图,调整会异步更新
            img.alt = record.fileName;
            // 生成调整后的图像数据 (同步用canvas)
            const adjustedDataUrl = generateAdjustedDataUrl(record.originalDataUrl, currentGamma);
            img.src = adjustedDataUrl;
            // 存下调整后的url到record中 (用于导出)
            record.adjustedDataUrl = adjustedDataUrl;
            wrapper.appendChild(img);
            card.appendChild(wrapper);
            // 底部信息
            const body = document.createElement('div');
            body.className = 'card-body';
            const nameSpan = document.createElement('span');
            nameSpan.className = 'filename';
            nameSpan.textContent = record.fileName;
            const removeBtn = document.createElement('button');
            removeBtn.className = 'remove-btn';
            removeBtn.innerHTML = '✕';
            removeBtn.setAttribute('aria-label', '移除图片');
            removeBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                removeImageById(record.id);
            });
            body.appendChild(nameSpan);
            body.appendChild(removeBtn);
            card.appendChild(body);
            grid.appendChild(card);
        });
        // 移除空状态
        showEmptyIfNeeded();
        updateStats();
    }
    // ========== canvas 调整 gamma (中间调) ==========
    function generateAdjustedDataUrl(dataUrl, gamma) {
        if (gamma === 1.0) return dataUrl; // 无变化直接返回原图 (节省性能)
        const img = new Image();
        // 同步方式:借助 canvas 同步绘制 (但 img 需要完全加载,这里用 dataUrl 已加载)
        // 但是为了通用性,实际需要用回调或者同步方式,由于我们已经有了 dataUrl,可以直接创建 Image 并立即绘制?
        // 但 Image 的 src 赋值是异步的,不过如果 dataUrl 是完整的,浏览器通常会同步解码(小图),但为了健壮性,
        // 使用 Promise 或同步方法?但我们在渲染循环中需要同步返回,所以采用 OffscreenCanvas 或者 2D context 的同步绘制?
        // 这里用 canvas 绘制,但必须确保图片已加载,比较可靠: 构造 Image 并设置 src,但 onload 后绘制,
        // 但为了函数同步返回,可以使用 canvas 的 drawImage 直接传入 Image 对象,Image 尚未解码则可能空白。
        // 替代方案:使用 Image 的 decode() 但也是异步,但是我们是生成调整后的预览,可以使用一个临时 canvas 并调用 drawImage,
        // 如果图片未加载,可以稍后更新?但交互中gamma滑动频繁,会导致闪烁。
        // 改进:我们将原始图片数据保持为 Image 对象或直接canvas 预处理,简单且稳健:每个 record 保存原始图片的 Image 对象 (预加载)
        // 但为了简洁,这里使用一个技巧:在添加图片时,我们将图片数据转为 Image 对象存入 record,并缓存原始 canvas 数据?
        // 更简洁:每次调整都创建一个新 Image 并等待?但渲染会卡顿,为了演示质量和流畅,我们设计为 在 gamma 变化时重新生成所有卡片,
        // 卡片中 img.src 采用同步生成的 dataUrl,这里用 canvas 绘制时如果图片未完成加载,采用上次的原始dataUrl 生成。
        // 解决方案:在添加图片时,将原始图片加载到 Image 对象,并保留原始 dataUrl 和 一个 HTMLImageElement。
        // 这里我们重构:每个 record 增加 originalImage (HTMLImageElement) 在添加时创建。
        // 为简化,我们可以在 gamma 调整时遍历 record 并基于 record.originalImage 绘制。
        // 但需要处理图片加载状态,我们选择在添加图片时等待图片加载完成,并保存 image 对象。
        // 重新设计 record: { id, fileName, originalDataUrl, image: HTMLImageElement, adjustedDataUrl? }
        // 由于已经实现到一半,这里采用快速修正:在 generateAdjustedDataUrl 中,record 有 image 属性则用,否则创建临时 Image。
        // 但该函数静态无法访问 records,我们修改为在渲染时传入 record,将 renderAllCards 重构。
        // 下面调整 renderAllCards 使用 record.image 直接绘制。
        // 由于我们之前保存 record 没有存 image,现在在添加图片时补充。
        // 改动较大但保证流畅,我们从头构建一个更可靠的架构:
    }
    // ========== 重新设计:使用 record.image 对象 ==========
    // 立即重构上方代码,重新定义渲染方式。
    // 新的渲染函数 (完全基于缓存 image 对象)
    function renderAllCardsStable() {
        // 清空卡片
        const cards = grid.querySelectorAll('.card');
        cards.forEach(c => c.remove());
        if (imageRecords.length === 0) {
            showEmptyIfNeeded();
            updateStats();
            return;
        }
        imageRecords.forEach((record) => {
            const card = document.createElement('div');
            card.className = 'card';
            card.dataset.id = record.id;
            const wrapper = document.createElement('div');
            wrapper.className = 'card-img-wrapper';
            const img = document.createElement('img');
            img.alt = record.fileName;
            // 根据当前 gamma 生成调整后的 dataUrl (同步)
            const adjusted = generateAdjustedDataUrlFromImage(record.image, currentGamma);
            img.src = adjusted;
            record.adjustedDataUrl = adjusted;   // 缓存
            wrapper.appendChild(img);
            card.appendChild(wrapper);
            const body = document.createElement('div');
            body.className = 'card-body';
            const nameSpan = document.createElement('span');
            nameSpan.className = 'filename';
            nameSpan.textContent = record.fileName;
            const removeBtn = document.createElement('button');
            removeBtn.className = 'remove-btn';
            removeBtn.innerHTML = '✕';
            removeBtn.addEventListener('click', () => removeImageById(record.id));
            body.appendChild(nameSpan);
            body.appendChild(removeBtn);
            card.appendChild(body);
            grid.appendChild(card);
        });
        showEmptyIfNeeded();
        updateStats();
    }
    // 基于 Image 对象生成 gamma 调整 dataUrl (同步)
    function generateAdjustedDataUrlFromImage(image, gamma) {
        if (gamma === 1.0) {
            // 直接返回原始 dataUrl (但记录中可能没有存原始dataUrl, 我们也可以从canvas导出)
            // 为性能,直接从image绘制导出原图
            const canvas = document.createElement('canvas');
            canvas.width = image.naturalWidth;
            canvas.height = image.naturalHeight;
            const ctx = canvas.getContext('2d');
            ctx.drawImage(image, 0, 0);
            return canvas.toDataURL('image/png');
        }
        const canvas = document.createElement('canvas');
        canvas.width = image.naturalWidth;
        canvas.height = image.naturalHeight;
        const ctx = canvas.getContext('2d');
        ctx.drawImage(image, 0, 0);
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const data = imageData.data;
        const gammaInv = 1 / gamma;
        for (let i = 0; i < data.length; i += 4) {
            // R, G, B
            data[i] = applyGamma(data[i], gammaInv);
            data[i+1] = applyGamma(data[i+1], gammaInv);
            data[i+2] = applyGamma(data[i+2], gammaInv);
            // alpha 不变
        }
        ctx.putImageData(imageData, 0, 0);
        return canvas.toDataURL('image/png');
    }
    function applyGamma(value, gammaInv) {
        const normalized = value / 255;
        // 避免负数
        const corrected = Math.pow(Math.max(0, normalized), gammaInv);
        return Math.min(255, Math.max(0, Math.round(corrected * 255)));
    }
    // ========== 添加图片 ==========
    function addFiles(files) {
        if (!files || files.length === 0) return;
        const validTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/bmp', 'image/gif', 'image/tiff'];
        const promises = [];
        for (const file of files) {
            if (!validTypes.includes(file.type) && !file.type.startsWith('image/')) continue;
            promises.push(loadImageFromFile(file));
        }
        if (promises.length === 0) return;
        Promise.allSettled(promises).then((results) => {
            let addedCount = 0;
            for (const result of results) {
                if (result.status === 'fulfilled') {
                    const { image, fileName, dataUrl } = result.value;
                    imageRecords.push({
                        id: nextId++,
                        fileName: fileName,
                        image: image,
                        originalDataUrl: dataUrl,
                        adjustedDataUrl: null,
                    });
                    addedCount++;
                }
            }
            if (addedCount > 0) {
                renderAllCardsStable();
                progressInfo.textContent = `已添加 ${addedCount} 张`;
            } else {
                progressInfo.textContent = '⚠️ 无法加载图片';
            }
        });
    }
    function loadImageFromFile(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = (e) => {
                const dataUrl = e.target.result;
                const img = new Image();
                img.onload = () => {
                    resolve({ image: img, fileName: file.name, dataUrl: dataUrl });
                };
                img.onerror = () => reject(new Error('图片解码失败'));
                img.src = dataUrl;
            };
            reader.onerror = () => reject(new Error('读取文件失败'));
            reader.readAsDataURL(file);
        });
    }
    // ========== 移除图片 ==========
    function removeImageById(id) {
        imageRecords = imageRecords.filter(r => r.id !== id);
        renderAllCardsStable();
        progressInfo.textContent = `已移除 1 张`;
    }
    // ========== 清空 ==========
    function clearAll() {
        if (imageRecords.length === 0) return;
        imageRecords = [];
        renderAllCardsStable();
        progressInfo.textContent = '已清空';
    }
    // ========== gamma 滑动事件 ==========
    gammaRange.addEventListener('input', function() {
        currentGamma = parseFloat(this.value);
        updateGammaDisplay();
        renderAllCardsStable();
        progressInfo.textContent = `Gamma = ${currentGamma.toFixed(2)}`;
    });
    // ========== 添加按钮 ==========
    addBtn.addEventListener('click', () => {
        fileInput.click();
    });
    fileInput.addEventListener('change', (e) => {
        const files = e.target.files;
        if (files && files.length > 0) {
            addFiles(files);
        }
        fileInput.value = ''; // 重置
    });
    // ========== 清空按钮 ==========
    clearBtn.addEventListener('click', clearAll);
    // ========== 批量导出 (ZIP) ==========
    downloadAllBtn.addEventListener('click', async function() {
        if (isProcessing) return;
        if (imageRecords.length === 0) {
            progressInfo.textContent = '⚠️ 没有图片可导出';
            return;
        }
        isProcessing = true;
        downloadAllBtn.disabled = true;
        progressInfo.textContent = '⏳ 正在生成...';
        try {
            const zip = new JSZip();
            const folder = zip.folder('gamma_adjusted');
            // 确保每个 record 有最新的 adjustedDataUrl (渲染时已缓存)
            // 但为了保险,重新生成一次
            const tasks = imageRecords.map((record, index) => {
                // 使用当前 gamma 生成
                const dataUrl = generateAdjustedDataUrlFromImage(record.image, currentGamma);
                const base64Data = dataUrl.split(',')[1];
                const ext = record.fileName.includes('.') ? record.fileName.split('.').pop() : 'png';
                // 简单过滤非字母数字
                const safeName = record.fileName.replace(/[^a-zA-Z0-9._-]/g, '_');
                folder.file(`gamma_${safeName}`, base64Data, { base64: true });
            });
            await Promise.all(tasks);
            const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } });
            const link = document.createElement('a');
            link.href = URL.createObjectURL(blob);
            link.download = `gamma_adjusted_${currentGamma.toFixed(2)}.zip`;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(link.href);
            progressInfo.textContent = `✅ 导出成功 (${imageRecords.length} 张)`;
        } catch (err) {
            console.error(err);
            progressInfo.textContent = '❌ 导出失败';
        } finally {
            isProcessing = false;
            downloadAllBtn.disabled = false;
        }
    });
    // ========== 初始化 ==========
    updateGammaDisplay();
    showEmptyIfNeeded();
    updateStats();
    // 支持拖放 (可选)
    const appEl = document.querySelector('.app');
    appEl.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; });
    appEl.addEventListener('drop', (e) => {
        e.preventDefault();
        const files = e.dataTransfer.files;
        if (files.length > 0) addFiles(files);
    });
})();
</script>
</body>
</html>

工具功能与操作说明

您可以通过滑动滑块调整Gamma值,并实时观察所有图片的中间调变化,以下是主要操作模块:

图片批量伽马工具中间调调整-第1张图片-电脑手机工具软件下载 - 免费实用工具合集 | 联启科技

  1. 图片管理:点击“添加图片”按钮或直接拖拽图片到工具区域,即可批量导入,每张图片右上角的“✕”按钮可单独移除。
  2. Gamma调整:拖动滑块(范围0.10至3.00),所有已添加图片的中间调对比度会同步更新,当前Gamma值会实时显示。
  3. 批量导出:调整满意后,点击“批量导出 (ZIP)”按钮,所有图片将按照当前Gamma设置打包下载,方便您保存或进一步使用。

标签: 中间调调整

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