做了一个图片批量编辑网站

网址:图片批量编辑(架设在github上,更新快)或 图片批量编辑

因为某些需要,找了半天也没在手机上找到能够批量编辑图片的软件或者是网站,有些网站只能上传几个图片,多了就得登陆或者开会员😅,所以就花了点时间手搓了个网站

优点:图片不会上传到服务器,所有操作均在本地进行,处理速度快,断网也能用

结束

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片批量编辑</title>
    <link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
    <style>
        header {
            background-color: #2c3e50;
            color: white;
            padding: 10px 0;
            text-align: center;
            font-size: 1.5rem;
        }
        .image-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
            gap: 10px;
            width: 100%;
            margin-top: 20px;
        }
        .image-grid img {
            width: 100%;
            height: auto;
            border-radius: 4px;
            object-fit: cover;
        }
        /* 底部按钮布局 */
        .navbar-bottom {
            position: fixed;
            bottom: 0;
            width: 100%;
            background-color: #f8f9fa;
            border-top: 1px solid #dee2e6;
            padding: 10px 0;
            text-align: center;
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            gap: 10px;
        }
        .navbar-bottom button {
            background: none;
            border: none;
            color: gray;
            font-size: 1rem;
            cursor: pointer;
        }
        .navbar-bottom button.active {
            color: lightblue;
        }
        /* 水印设置弹窗样式 */
        #watermark-modal {
            display: none;
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border: 1px solid #ccc;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
            z-index: 1000;
        }
        #watermark-modal h3 {
            margin-top: 0;
        }
        #watermark-modal label {
            display: block;
            margin-top: 10px;
        }
        #watermark-modal input[type="range"] {
            width: 100%;
        }
        #watermark-modal button {
            margin-top: 20px;
            padding: 10px 20px;
            cursor: pointer;
        }
    </style>
</head>
<body>
<header>
    <h1>图片批量编辑</h1>
</header>
<main id="main-container">
    <input type="file" id="image-input" accept="image/*" multiple style="display:none;">
    <div class="image-grid" id="image-grid"></div>
</main>
<div class="navbar-bottom">
    <button class="btn-default" id="rotate-button">
        <span class="icon">↻</span><br>旋转
    </button>
    <button class="btn-default" id="mirror-button">
        <span class="icon">⇆</span><br>镜像
    </button>
    <button class="btn-default" id="rotate-mirror-button">
        <span class="icon">↔️</span><br>旋转镜像
    </button>
    <button class="btn-default" id="watermark-button">
        <span class="icon">💧</span><br>水印
    </button>
    <button class="btn-save" id="save-button">
        <span class="icon">💾</span><br>保存图片
    </button>
    <button class="btn-clear" id="clear-button">
        <span class="icon">🗑️</span><br>清空
    </button>
</div>
<div id="watermark-modal" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border: 1px solid #ccc; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); z-index: 1000;">
    <h3>水印设置</h3>
    <a href="https://github.com/joyqi/sfz">Github地址</a></article>
    <label for="text">输入需要打水印的文字</label>
    <p>
        <input type="text" id="text" value="经供参考" autocomplete="off" maxlength="30">
    </p>
    <label for="color">颜色</label>
    <p>
        <input type="color" id="color" pattern="#[0-9A-Fa-f]{6}" autocomplete="off" value="#ffffff">
    </p>
    <label for="alpha">透明度</label>
    <p>
        <input type="range" id="alpha" min="0" max="1" step="0.05" autocomplete="off" value="0.15">
    </p>
    <label for="angle">角度</label>
    <p>
        <input type="range" id="angle" min="-90" max="90" step="3" autocomplete="off" value="45">
    </p>
    <label for="space">间隔</label>
    <p>
        <input type="range" id="space" min="1" max="8" step="0.2" autocomplete="off" value="2">
    </p>
    <label for="size">字号</label>
    <p>
        <input type="range" id="size" min="0.5" max="3" step="0.05" autocomplete="off" value="1">
    </p>
    <button id="apply-watermark">应用水印</button>
    <button id="close-modal">关闭</button>
</div>
<script src="https://unpkg.com/filepond/dist/filepond.js"></script>
<script>
const pond = FilePond.create(document.querySelector('input[type="file"]'), {
    allowMultiple: true, // 允许多选
    acceptedFileTypes: ['image/*'],
    labelIdle: '拖放图片或点击选择图片',
    allowRemove: true,
});
pond.on('addfile', (error, file) => {
    if (error) {
        console.error('文件添加失败:', error);
        return;
    }
    const imageGrid = document.getElementById('image-grid');
    const existingImage = Array.from(imageGrid.querySelectorAll('img')).find(
        img => img.dataset.fileId === file.id
    );
    if (!existingImage) {
        const img = document.createElement('img');
        img.src = URL.createObjectURL(file.file);
        img.dataset.rotate = 0; // 初始化旋转角度
        img.dataset.mirror = 'false'; // 初始化镜像状态
        img.dataset.fileId = file.id; // 为图片添加唯一标识
        imageGrid.appendChild(img);
        document.getElementById('image-grid').style.display = 'grid';
    }
});
// 监听文件删除事件
pond.on('removefile', (error, file) => {
    if (error) {
        console.error('文件删除失败:', error);
        return;
    }
    const imageGrid = document.getElementById('image-grid');
    const images = imageGrid.querySelectorAll('img');
    images.forEach(img => {
        if (img.dataset.fileId === file.id) { // 通过唯一标识匹配
            img.remove();
        }
    });
    if (imageGrid.children.length === 0) {
        imageGrid.style.display = 'none';
    }
});
// 绑定按钮事件
document.getElementById('rotate-button').addEventListener('click', rotateImages);
document.getElementById('mirror-button').addEventListener('click', mirrorImages);
document.getElementById('rotate-mirror-button').addEventListener('click', rotateMirrorImages);
document.getElementById('save-button').addEventListener('click', saveImages);
document.getElementById('clear-button').addEventListener('click', clearImages);
// 旋转图片(每次旋转90°)
function rotateImages() {
    const images = document.querySelectorAll('.image-grid img');
    images.forEach(img => {
        let currentRotate = parseInt(img.dataset.rotate) || 0;
        currentRotate = (currentRotate + 90) % 360;
        img.dataset.rotate = currentRotate; // 更新旋转角度
        applyTransform(img);
    });
}
// 镜像图片
function mirrorImages() {
    const images = document.querySelectorAll('.image-grid img');
    images.forEach(img => {
        const isMirrored = img.dataset.mirror === 'true';
        img.dataset.mirror = !isMirrored; // 切换镜像状态
        applyTransform(img);
    });
}
// 旋转并镜像图片
function rotateMirrorImages() {
    const images = document.querySelectorAll('.image-grid img');
    images.forEach(img => {
        let currentRotate = parseInt(img.dataset.rotate) || 0;
        currentRotate = (currentRotate + 180) % 360;
        img.dataset.rotate = currentRotate;
        const isMirrored = img.dataset.mirror === 'true';
        img.dataset.mirror = !isMirrored;
        applyTransform(img);
    });
}
// 旋转 + 镜像
function applyTransform(img) {
    const rotate = parseInt(img.dataset.rotate) || 0;
    const isMirrored = img.dataset.mirror === 'true';
    img.style.transform = `rotate(${rotate}deg) ${isMirrored ? 'scaleX(-1)' : ''}`;
}
document.getElementById('watermark-button').addEventListener('click', () => {
    document.getElementById('watermark-modal').style.display = 'block';
});
document.getElementById('close-modal').addEventListener('click', () => {
    document.getElementById('watermark-modal').style.display = 'none';
});
document.getElementById('apply-watermark').addEventListener('click', () => {
    const watermarkText = document.getElementById('text').value;
    const watermarkColor = document.getElementById('color').value;
    const watermarkAlpha = parseFloat(document.getElementById('alpha').value);
    const watermarkAngle = parseFloat(document.getElementById('angle').value);
    const watermarkSpace = parseFloat(document.getElementById('space').value);
    const watermarkSize = parseFloat(document.getElementById('size').value);
    addWatermark(watermarkText, watermarkColor, watermarkAlpha, watermarkAngle, watermarkSpace, watermarkSize);
    document.getElementById('watermark-modal').style.display = 'none';
});
// 添加水印功能
function addWatermark(text, color, alpha, angle, space, size) {
    const images = document.querySelectorAll('.image-grid img');
    if (images.length === 0) {
        alert('请先导入图片!');
        return;
    }
    images.forEach(img => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        canvas.width = img.naturalWidth;
        canvas.height = img.naturalHeight;
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        // 设置水印样式
        ctx.font = `${canvas.width * 0.05 * size}px Arial`; // 根据图片宽度和字号比例设置字体大小
        ctx.fillStyle = `rgba(${parseInt(color.slice(1, 3), 16)}, ${parseInt(color.slice(3, 5), 16)}, ${parseInt(color.slice(5, 7), 16)}, ${alpha})`;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.translate(canvas.width / 2, canvas.height / 2);
        ctx.rotate((angle * Math.PI) / 180);
        const textWidth = ctx.measureText(text).width;
        const textHeight = parseInt(ctx.font, 10);
        for (let x = -canvas.width; x < canvas.width * 2; x += textWidth * space) {
            for (let y = -canvas.height; y < canvas.height * 2; y += textHeight * space) {
                ctx.fillText(text, x, y);
            }
        }
        img.src = canvas.toDataURL('image/png');
    });
}
//打包为ZIP文件
async function saveImages() {
    const images = document.querySelectorAll('.image-grid img');
    if (images.length === 0) {
        alert('请先导入图片!');
        return;
    }
    const zip = new JSZip();
    const folder = zip.folder('edited-images');
    await Promise.all(Array.from(images).map(async (img, index) => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        //限制最大宽度或高度为 1024px
        const maxSize = 1024;
        let width = img.naturalWidth;
        let height = img.naturalHeight;
        if (width > maxSize || height > maxSize) {
            const ratio = Math.min(maxSize / width, maxSize / height);
            width = Math.floor(width * ratio);
            height = Math.floor(height * ratio);
        }
        canvas.width = width;
        canvas.height = height;
        ctx.save();
        ctx.translate(canvas.width / 2, canvas.height / 2);
        ctx.rotate((parseInt(img.dataset.rotate) || 0) * Math.PI / 180);
        if (img.dataset.mirror === 'true') {
            ctx.scale(-1, 1);
        }
        ctx.drawImage(img, -canvas.width / 2, -canvas.height / 2, canvas.width, canvas.height);
        ctx.restore();
        // 将 canvas 转换为 Blob 并添加到 ZIP
        return new Promise(resolve => {
            canvas.toBlob(blob => {
                folder.file(`image-${index + 1}.png`, blob);
                resolve();
            }, 'image/png', 0.8);
        });
    }));
    zip.generateAsync({ type: 'blob' }).then(content => {
        saveAs(content, 'edited-images.zip');
    });
}
// 清空图片
function clearImages() {
    const imageGrid = document.getElementById('image-grid');
    imageGrid.innerHTML = '';
    document.getElementById('image-grid').style.display = 'none';
    pond.removeFiles();
}
</script>
</body>
</html>
上一篇
下一篇