图片批量翻转工具水平垂直

联启 电脑工具 12
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">图片批量翻转工具 · 水平垂直</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        body {
            font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
            background: #f5f7fb;
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 16px;
        }
        .card {
            max-width: 820px;
            width: 100%;
            background: #ffffff;
            border-radius: 32px;
            box-shadow: 0 20px 40px rgba(0, 10, 30, 0.1);
            padding: 28px 24px 32px;
            transition: all 0.2s ease;
        }
        h1 {
            font-size: 28px;
            font-weight: 600;
            letter-spacing: -0.3px;
            color: #0b1a33;
            display: flex;
            align-items: center;
            gap: 10px;
            margin-bottom: 8px;
        }
        h1 small {
            font-size: 18px;
            font-weight: 400;
            color: #64748b;
            margin-left: 4px;
        }
        .subtitle {
            color: #475569;
            font-size: 16px;
            border-left: 4px solid #3b82f6;
            padding-left: 14px;
            margin-bottom: 28px;
            line-height: 1.4;
            background: #f8fafc;
            border-radius: 0 12px 12px 0;
        }
        .control-bar {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            justify-content: space-between;
            gap: 16px 12px;
            background: #f1f5f9;
            padding: 16px 18px;
            border-radius: 40px;
            margin-bottom: 24px;
        }
        .flip-options {
            display: flex;
            flex-wrap: wrap;
            gap: 10px 16px;
            align-items: center;
        }
        .flip-options label {
            display: flex;
            align-items: center;
            gap: 8px;
            font-weight: 500;
            color: #1e293b;
            background: white;
            padding: 6px 16px 6px 12px;
            border-radius: 40px;
            box-shadow: 0 2px 6px rgba(0,0,0,0.02);
            cursor: pointer;
            transition: 0.15s;
            border: 1px solid #d1d9e6;
        }
        .flip-options label:hover {
            border-color: #94a3b8;
            background: #fcfdff;
        }
        .flip-options input[type="checkbox"] {
            width: 18px;
            height: 18px;
            accent-color: #2563eb;
            border-radius: 4px;
            cursor: pointer;
        }
        .action-buttons {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            align-items: center;
        }
        .btn {
            background: white;
            border: 1px solid #cdd9ed;
            color: #1e293b;
            padding: 8px 20px;
            border-radius: 40px;
            font-weight: 500;
            font-size: 14px;
            display: inline-flex;
            align-items: center;
            gap: 6px;
            cursor: pointer;
            transition: 0.15s;
            box-shadow: 0 2px 4px rgba(0,0,0,0.02);
            letter-spacing: 0.3px;
        }
        .btn-primary {
            background: #1e3a8a;
            border-color: #1e3a8a;
            color: white;
            box-shadow: 0 4px 8px rgba(30, 58, 138, 0.2);
        }
        .btn-primary:hover {
            background: #172554;
            border-color: #172554;
            transform: scale(0.97);
        }
        .btn-outline {
            background: transparent;
            border: 1px solid #b9c7dd;
        }
        .btn-outline:hover {
            background: #eef2f7;
            border-color: #94a3b8;
        }
        .btn:active {
            transform: scale(0.96);
        }
        .btn svg {
            width: 18px;
            height: 18px;
            fill: none;
            stroke: currentColor;
            stroke-width: 2.2;
            stroke-linecap: round;
            stroke-linejoin: round;
        }
        .upload-area {
            border: 2px dashed #cbd5e1;
            border-radius: 28px;
            padding: 32px 12px;
            text-align: center;
            background: #fafcfd;
            transition: 0.2s;
            cursor: pointer;
            margin-bottom: 20px;
        }
        .upload-area:hover {
            border-color: #3b82f6;
            background: #f0f7ff;
        }
        .upload-icon {
            font-size: 48px;
            line-height: 1;
            margin-bottom: 6px;
            opacity: 0.7;
        }
        .upload-text {
            font-weight: 500;
            color: #1e293b;
        }
        .upload-hint {
            font-size: 14px;
            color: #64748b;
            margin-top: 6px;
        }
        #fileInput {
            display: none;
        }
        .preview-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
            gap: 16px;
            margin-top: 24px;
            margin-bottom: 8px;
        }
        .preview-item {
            background: #f8fafc;
            border-radius: 20px;
            padding: 12px 8px 10px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.04);
            display: flex;
            flex-direction: column;
            align-items: center;
            transition: 0.1s;
            border: 1px solid #e9edf2;
        }
        .preview-item canvas {
            width: 100%;
            aspect-ratio: 1 / 1;
            object-fit: contain;
            border-radius: 12px;
            background: #ffffff;
            display: block;
            box-shadow: 0 2px 6px rgba(0,0,0,0.03);
        }
        .preview-name {
            font-size: 13px;
            font-weight: 500;
            margin-top: 8px;
            color: #1e293b;
            text-overflow: ellipsis;
            overflow: hidden;
            white-space: nowrap;
            max-width: 100%;
            padding: 0 4px;
        }
        .preview-size {
            font-size: 11px;
            color: #64748b;
        }
        .empty-state {
            color: #64748b;
            text-align: center;
            padding: 40px 10px;
            font-size: 15px;
        }
        .footer-actions {
            display: flex;
            flex-wrap: wrap;
            justify-content: flex-end;
            gap: 12px;
            margin-top: 28px;
            border-top: 1px solid #e2e8f0;
            padding-top: 22px;
        }
        .badge {
            background: #eef2ff;
            color: #1e3a8a;
            padding: 2px 14px;
            border-radius: 30px;
            font-size: 14px;
            font-weight: 500;
        }
        .btn-success {
            background: #0f973d;
            border-color: #0f973d;
            color: white;
            box-shadow: 0 4px 10px rgba(15, 151, 61, 0.25);
        }
        .btn-success:hover {
            background: #0c7c32;
            border-color: #0c7c32;
        }
        @media (max-width: 500px) {
            .card { padding: 20px 14px; }
            .control-bar { flex-direction: column; align-items: stretch; }
            .flip-options { justify-content: center; }
            .action-buttons { justify-content: center; }
            h1 { font-size: 24px; }
        }
    </style>
</head>
<body>
<div class="card">
    <h1>🔄 图片翻转 <small>批量 · 水平垂直</small></h1>
    <div class="subtitle">同时翻转多张图片,支持水平(H)、垂直(V)或组合翻转,预览即时生成,一键批量下载。</div>
    <!-- 控制栏 -->
    <div class="control-bar">
        <div class="flip-options">
            <label>
                <input type="checkbox" id="flipH" checked> 水平翻转
            </label>
            <label>
                <input type="checkbox" id="flipV"> 垂直翻转
            </label>
            <span class="badge" id="modeLabel">水平</span>
        </div>
        <div class="action-buttons">
            <button class="btn btn-outline" id="clearBtn" title="清空所有图片">
                <svg viewBox="0 0 24 24"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6"/></svg>
                清空
            </button>
        </div>
    </div>
    <!-- 上传区域 -->
    <div class="upload-area" id="dropZone">
        <div class="upload-icon">📸</div>
        <div class="upload-text">点击选择图片 或 拖放图片到这里</div>
        <div class="upload-hint">支持 JPG / PNG / WebP / BMP · 可多选</div>
    </div>
    <input type="file" id="fileInput" accept="image/*" multiple>
    <!-- 预览网格 -->
    <div id="previewContainer" class="preview-grid"></div>
    <!-- 底部批量下载 -->
    <div class="footer-actions" id="footerActions" style="display: none;">
        <span id="fileCount" class="badge">0 张图片</span>
        <button class="btn btn-success" id="downloadAllBtn">
            ⬇️ 批量下载翻转后图片
        </button>
    </div>
</div>
<script>
    (function() {
        // DOM 元素
        const fileInput = document.getElementById('fileInput');
        const dropZone = document.getElementById('dropZone');
        const previewContainer = document.getElementById('previewContainer');
        const clearBtn = document.getElementById('clearBtn');
        const downloadAllBtn = document.getElementById('downloadAllBtn');
        const footerActions = document.getElementById('footerActions');
        const fileCount = document.getElementById('fileCount');
        const flipHCheck = document.getElementById('flipH');
        const flipVCheck = document.getElementById('flipV');
        const modeLabel = document.getElementById('modeLabel');
        // 状态存储:所有图片的原始数据 (File 对象 + 预览 URL)
        let imageFiles = [];          // 存储 File 对象
        let imagePreviewURLs = [];    // 存储 createObjectURL 生成的 url (用于预览)
        let imageDataMapping = [];    // 同时存储原始文件名和类型
        // 更新模式标签
        function updateModeLabel() {
            const h = flipHCheck.checked;
            const v = flipVCheck.checked;
            if (h && v) modeLabel.textContent = '水平+垂直';
            else if (h) modeLabel.textContent = '水平';
            else if (v) modeLabel.textContent = '垂直';
            else modeLabel.textContent = '无翻转 (原图)';
            // 重新渲染所有预览 (翻转参数改变)
            renderAllPreviews();
        }
        // 监听翻转复选框
        flipHCheck.addEventListener('change', updateModeLabel);
        flipVCheck.addEventListener('change', updateModeLabel);
        // ----- 渲染预览 (根据当前翻转设置) -----
        function renderAllPreviews() {
            if (!previewContainer) return;
            // 清空预览区
            previewContainer.innerHTML = '';
            if (imageFiles.length === 0) {
                footerActions.style.display = 'none';
                fileCount.textContent = '0 张图片';
                // 显示空状态
                const emptyDiv = document.createElement('div');
                emptyDiv.className = 'empty-state';
                emptyDiv.style.gridColumn = '1 / -1';
                emptyDiv.textContent = '✨ 拖入或选择图片开始翻转';
                previewContainer.appendChild(emptyDiv);
                return;
            }
            // 显示底部操作
            footerActions.style.display = 'flex';
            fileCount.textContent = `${imageFiles.length} 张图片`;
            const hFlip = flipHCheck.checked;
            const vFlip = flipVCheck.checked;
            // 遍历所有图片生成 canvas 预览
            imageFiles.forEach((file, index) => {
                const img = new Image();
                const objectURL = imagePreviewURLs[index];
                if (!objectURL) return;
                img.onload = function() {
                    // 确保 index 依然有效 (如果图片被清空则忽略)
                    if (index >= imageFiles.length) return;
                    // 创建 canvas
                    const canvas = document.createElement('canvas');
                    const ctx = canvas.getContext('2d');
                    canvas.width = img.naturalWidth || img.width;
                    canvas.height = img.naturalHeight || img.height;
                    // 应用翻转
                    ctx.save();
                    // 水平翻转: scale(-1, 1) 且 translate(-width, 0)
                    // 垂直翻转: scale(1, -1) 且 translate(0, -height)
                    let scaleX = 1, scaleY = 1;
                    let transX = 0, transY = 0;
                    if (hFlip) { scaleX = -1; transX = -canvas.width; }
                    if (vFlip) { scaleY = -1; transY = -canvas.height; }
                    ctx.translate(transX, transY);
                    ctx.scale(scaleX, scaleY);
                    ctx.drawImage(img, 0, 0);
                    ctx.restore();
                    // 创建预览容器
                    const itemDiv = document.createElement('div');
                    itemDiv.className = 'preview-item';
                    // 插入 canvas
                    const previewCanvas = canvas.cloneNode(true);
                    itemDiv.appendChild(previewCanvas);
                    // 文件名 + 尺寸
                    const nameSpan = document.createElement('div');
                    nameSpan.className = 'preview-name';
                    const baseName = file.name.length > 16 ? file.name.slice(0, 12) + '…' : file.name;
                    nameSpan.textContent = baseName;
                    itemDiv.appendChild(nameSpan);
                    const sizeSpan = document.createElement('div');
                    sizeSpan.className = 'preview-size';
                    sizeSpan.textContent = `${canvas.width} × ${canvas.height}`;
                    itemDiv.appendChild(sizeSpan);
                    // 如果预览项数量对得上,才追加 (防止异步乱序)
                    // 但多个图片加载完成顺序可能不一致,我们用 dataset 标记 index 来插入正确位置?
                    // 简便方式:全部追加到最后,但顺序可能乱,更优:根据 index 插入到对应位置
                    // 但我们保存所有item的顺序为files顺序,采用插入到指定位置方式
                    const existingItems = previewContainer.children;
                    let inserted = false;
                    for (let i = 0; i < existingItems.length; i++) {
                        const item = existingItems[i];
                        const idx = parseInt(item.dataset.index);
                        if (isNaN(idx) || idx > index) {
                            previewContainer.insertBefore(itemDiv, item);
                            inserted = true;
                            break;
                        }
                    }
                    if (!inserted) {
                        previewContainer.appendChild(itemDiv);
                    }
                    itemDiv.dataset.index = index;
                };
                img.onerror = function() {
                    // 图片加载失败占位
                    const itemDiv = document.createElement('div');
                    itemDiv.className = 'preview-item';
                    itemDiv.style.justifyContent = 'center';
                    itemDiv.style.padding = '20px 0';
                    itemDiv.textContent = '⚠️ 加载失败';
                    previewContainer.appendChild(itemDiv);
                };
                // 重新设置 src 触发加载 (使用缓存的 URL)
                img.src = objectURL;
            });
        }
        // ----- 添加文件 -----
        function addFiles(files) {
            if (!files || files.length === 0) return;
            // 过滤仅图片
            const validFiles = Array.from(files).filter(file => file.type.startsWith('image/'));
            if (validFiles.length === 0) {
                alert('请选择图片文件 (JPG, PNG, WebP, BMP 等)');
                return;
            }
            // 存储文件以及生成预览URL
            validFiles.forEach(file => {
                imageFiles.push(file);
                const url = URL.createObjectURL(file);
                imagePreviewURLs.push(url);
                imageDataMapping.push({ name: file.name, type: file.type });
            });
            // 重新渲染所有预览 (异步加载)
            renderAllPreviews();
        }
        // ----- 清空所有图片 -----
        function clearAll() {
            // 释放所有 objectURL
            imagePreviewURLs.forEach(url => {
                if (url) URL.revokeObjectURL(url);
            });
            imageFiles = [];
            imagePreviewURLs = [];
            imageDataMapping = [];
            previewContainer.innerHTML = '';
            footerActions.style.display = 'none';
            fileCount.textContent = '0 张图片';
            // 显示空状态
            const emptyDiv = document.createElement('div');
            emptyDiv.className = 'empty-state';
            emptyDiv.style.gridColumn = '1 / -1';
            emptyDiv.textContent = '✨ 拖入或选择图片开始翻转';
            previewContainer.appendChild(emptyDiv);
        }
        // ----- 批量下载 (每个文件单独下载) -----
        function downloadAll() {
            if (imageFiles.length === 0) {
                alert('没有图片可下载');
                return;
            }
            const hFlip = flipHCheck.checked;
            const vFlip = flipVCheck.checked;
            // 用 Promise 处理所有图片的异步导出
            const downloadPromises = imageFiles.map((file, index) => {
                return new Promise((resolve, reject) => {
                    const img = new Image();
                    const url = imagePreviewURLs[index];
                    if (!url) return reject('No URL');
                    img.onload = function() {
                        const canvas = document.createElement('canvas');
                        const ctx = canvas.getContext('2d');
                        canvas.width = img.naturalWidth || img.width;
                        canvas.height = img.naturalHeight || img.height;
                        ctx.save();
                        let scaleX = 1, scaleY = 1;
                        let transX = 0, transY = 0;
                        if (hFlip) { scaleX = -1; transX = -canvas.width; }
                        if (vFlip) { scaleY = -1; transY = -canvas.height; }
                        ctx.translate(transX, transY);
                        ctx.scale(scaleX, scaleY);
                        ctx.drawImage(img, 0, 0);
                        ctx.restore();
                        // 转为 Blob (尽量保持原格式)
                        const mimeType = file.type || 'image/png';
                        // 如果浏览器不支持原始格式,降级为 png
                        const outputType = (mimeType === 'image/webp' || mimeType === 'image/bmp' || mimeType === 'image/jpeg' || mimeType === 'image/png') ? mimeType : 'image/png';
                        canvas.toBlob((blob) => {
                            if (blob) {
                                // 构建新文件名: 原名称_翻转后缀
                                const dotIndex = file.name.lastIndexOf('.');
                                const baseName = dotIndex > 0 ? file.name.slice(0, dotIndex) : file.name;
                                const ext = dotIndex > 0 ? file.name.slice(dotIndex) : '.png';
                                const flipTag = [];
                                if (hFlip) flipTag.push('H');
                                if (vFlip) flipTag.push('V');
                                const suffix = flipTag.length ? '_' + flipTag.join('') : '_origin';
                                const newFileName = `${baseName}${suffix}${ext}`;
                                resolve({ blob, fileName: newFileName });
                            } else {
                                reject('Blob conversion failed');
                            }
                        }, outputType, 0.92);
                    };
                    img.onerror = () => reject('Image load error');
                    img.src = url;
                });
            });
            // 逐个下载 (触发浏览器下载)
            Promise.allSettled(downloadPromises).then(results => {
                let successCount = 0;
                results.forEach((result, idx) => {
                    if (result.status === 'fulfilled' && result.value) {
                        const { blob, fileName } = result.value;
                        const link = document.createElement('a');
                        link.href = URL.createObjectURL(blob);
                        link.download = fileName;
                        document.body.appendChild(link);
                        link.click();
                        document.body.removeChild(link);
                        // 释放临时 blob URL
                        setTimeout(() => URL.revokeObjectURL(link.href), 100);
                        successCount++;
                    } else {
                        console.warn(`下载失败: 索引${idx}`, result.reason);
                    }
                });
                if (successCount === 0) {
                    alert('下载失败,请重试');
                } else {
                    // 可提示
                }
            });
        }
        // ----- 事件绑定 -----
        // 点击上传区域触发 file input
        dropZone.addEventListener('click', () => {
            fileInput.click();
        });
        fileInput.addEventListener('change', (e) => {
            addFiles(e.target.files);
            fileInput.value = ''; // 允许重复选择相同文件
        });
        // 拖拽上传
        dropZone.addEventListener('dragover', (e) => {
            e.preventDefault();
            dropZone.style.borderColor = '#2563eb';
            dropZone.style.background = '#eef2ff';
        });
        dropZone.addEventListener('dragleave', (e) => {
            e.preventDefault();
            dropZone.style.borderColor = '#cbd5e1';
            dropZone.style.background = '#fafcfd';
        });
        dropZone.addEventListener('drop', (e) => {
            e.preventDefault();
            dropZone.style.borderColor = '#cbd5e1';
            dropZone.style.background = '#fafcfd';
            if (e.dataTransfer.files.length > 0) {
                addFiles(e.dataTransfer.files);
            }
        });
        // 清空
        clearBtn.addEventListener('click', clearAll);
        // 批量下载
        downloadAllBtn.addEventListener('click', downloadAll);
        // 初始化空状态
        clearAll();
        // 额外:当窗口尺寸变化时,不额外操作,grid自动适应
        // 但如果图片数量很多,渲染异步可能重复,但我们做了index保护
        // 如果多次切换翻转,需要重新渲染;但 renderAllPreviews 会先清空再全部重建,可能会闪烁。
        // 但为简单起见,直接清空重建。
        // 优化:防止快速切换时多次重建,加防抖?但影响不大。
        // 采用防抖简单处理
        let debounceTimer;
        function debounceRender() {
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(() => {
                renderAllPreviews();
            }, 80);
        }
        // 覆写 updateModeLabel 的渲染为防抖
        const originalRender = updateModeLabel;
        updateModeLabel = function() {
            const h = flipHCheck.checked;
            const v = flipVCheck.checked;
            if (h && v) modeLabel.textContent = '水平+垂直';
            else if (h) modeLabel.textContent = '水平';
            else if (v) modeLabel.textContent = '垂直';
            else modeLabel.textContent = '无翻转 (原图)';
            debounceRender();
        };
        // 重新绑定事件(移除之前的监听,重新添加新函数)
        flipHCheck.removeEventListener('change', originalRender);
        flipVCheck.removeEventListener('change', originalRender);
        flipHCheck.addEventListener('change', updateModeLabel);
        flipVCheck.addEventListener('change', updateModeLabel);
        // 首次调用更新
        updateModeLabel();
    })();
</script>
</body>
</html>

工具功能与操作流程

您可以通过简单的勾选和拖拽,快速完成图片的批量翻转与下载,其核心操作流程如下:

图片批量翻转工具水平垂直-第1张图片-电脑手机工具软件下载 - 免费实用工具合集 | 联启科技

  1. 选择翻转模式:通过勾选“水平翻转”和/或“垂直翻转”复选框,您可以自由组合翻转效果,当前模式会实时显示在标签中。
  2. 导入图片:您可以通过点击上传区域或直接拖拽图片文件到该区域来导入图片,工具支持 JPG、PNG 等常见格式,并允许一次选择多张图片。
  3. 预览与下载:所有图片导入后,会立即根据当前翻转设置生成预览,确认效果后,点击底部的“批量下载翻转后图片”按钮,即可将每张图片按所选翻转模式处理并逐一保存到本地。

标签: 水平垂直翻转

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