图片切割工具:智能分割长图并控制文件大小

发布于:2025-06-08 ⋅ 阅读:(15) ⋅ 点赞:(0)

写在前面

这是一个直观易用的图片切割工具,用户上传长图后自动分割成多个不超过5MB的片段,保持原始宽度不变,自己用的,顺带发表一下,不喜勿喷!

设计思路

  1. 创建简洁的UI,包含上传区域和结果展示区

  2. 实现智能分割算法,保持原始宽度,动态计算高度

  3. 确保每个片段不超过5MB

  4. 使用Canvas进行图片处理和压缩

  5. 添加下载功能,支持下载所有切割后的图片

效果预览

最终实现代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>智能图片切割工具</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
            color: #fff;
            min-height: 100vh;
            padding: 20px;
        }
        
        .container {
            max-width: 1000px;
            margin: 0 auto;
            padding: 20px;
        }
        
        header {
            text-align: center;
            padding: 30px 0;
            margin-bottom: 30px;
            background: rgba(0, 0, 0, 0.3);
            border-radius: 15px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
            backdrop-filter: blur(10px);
        }
        
        h1 {
            font-size: 2.5rem;
            margin-bottom: 15px;
            text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
        }
        
        .subtitle {
            font-size: 1.2rem;
            opacity: 0.9;
            max-width: 700px;
            margin: 0 auto;
            line-height: 1.6;
        }
        
        .card {
            background: rgba(255, 255, 255, 0.15);
            border-radius: 15px;
            padding: 30px;
            margin-bottom: 30px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
            backdrop-filter: blur(10px);
        }
        
        .upload-area {
            border: 3px dashed rgba(255, 255, 255, 0.4);
            border-radius: 10px;
            padding: 40px 20px;
            text-align: center;
            cursor: pointer;
            transition: all 0.3s ease;
            position: relative;
        }
        
        .upload-area:hover {
            background: rgba(255, 255, 255, 0.1);
            border-color: rgba(255, 255, 255, 0.7);
        }
        
        .upload-area i {
            font-size: 60px;
            margin-bottom: 20px;
            display: block;
            color: rgba(255, 255, 255, 0.8);
        }
        
        .upload-area h2 {
            margin-bottom: 15px;
        }
        
        .upload-area p {
            margin-bottom: 20px;
            opacity: 0.8;
        }
        
        .btn {
            background: linear-gradient(to right, #ff416c, #ff4b2b);
            color: white;
            border: none;
            padding: 12px 30px;
            font-size: 1.1rem;
            border-radius: 50px;
            cursor: pointer;
            transition: all 0.3s ease;
            display: inline-block;
            box-shadow: 0 4px 15px rgba(255, 75, 43, 0.4);
        }
        
        .btn:hover {
            transform: translateY(-3px);
            box-shadow: 0 6px 20px rgba(255, 75, 43, 0.6);
        }
        
        .btn:active {
            transform: translateY(1px);
        }
        
        .btn-secondary {
            background: linear-gradient(to right, #2193b0, #6dd5ed);
            box-shadow: 0 4px 15px rgba(33, 147, 176, 0.4);
        }
        
        .btn-secondary:hover {
            box-shadow: 0 6px 20px rgba(33, 147, 176, 0.6);
        }
        
        .btn:disabled {
            background: #999;
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }
        
        input[type="file"] {
            display: none;
        }
        
        .preview-container {
            display: none;
        }
        
        .image-info {
            display: flex;
            justify-content: space-between;
            margin-bottom: 20px;
            background: rgba(0, 0, 0, 0.2);
            padding: 15px;
            border-radius: 10px;
        }
        
        .image-info div {
            text-align: center;
            flex: 1;
        }
        
        .image-info h3 {
            font-size: 1.1rem;
            margin-bottom: 8px;
            opacity: 0.8;
        }
        
        .image-info p {
            font-size: 1.3rem;
            font-weight: bold;
        }
        
        .results-container {
            display: none;
            margin-top: 30px;
        }
        
        .results-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
        }
        
        .slices-container {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
            gap: 20px;
        }
        
        .slice-card {
            background: rgba(255, 255, 255, 0.1);
            border-radius: 10px;
            overflow: hidden;
            box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
            transition: transform 0.3s ease;
        }
        
        .slice-card:hover {
            transform: translateY(-5px);
        }
        
        .slice-img {
            width: 100%;
            display: block;
            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
        }
        
        .slice-info {
            padding: 15px;
            text-align: center;
        }
        
        .slice-name {
            font-weight: bold;
            margin-bottom: 8px;
            font-size: 1.1rem;
        }
        
        .slice-size {
            font-size: 0.9rem;
            opacity: 0.8;
        }
        
        .progress-container {
            margin: 20px 0;
            height: 8px;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 4px;
            overflow: hidden;
        }
        
        .progress-bar {
            height: 100%;
            background: linear-gradient(to right, #00b09b, #96c93d);
            width: 0%;
            transition: width 0.5s ease;
        }
        
        .status-text {
            text-align: center;
            margin: 10px 0;
            font-style: italic;
            opacity: 0.8;
        }
        
        .loading {
            text-align: center;
            padding: 30px;
            display: none;
        }
        
        .spinner {
            width: 50px;
            height: 50px;
            border: 5px solid rgba(255, 255, 255, 0.3);
            border-radius: 50%;
            border-top-color: #fff;
            animation: spin 1s ease-in-out infinite;
            margin: 0 auto 20px;
        }
        
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
        
        .action-buttons {
            display: flex;
            justify-content: center;
            gap: 15px;
            margin-top: 20px;
            flex-wrap: wrap;
        }
        
        footer {
            text-align: center;
            margin-top: 40px;
            padding: 20px;
            font-size: 0.9rem;
            opacity: 0.7;
        }
        
        @media (max-width: 768px) {
            .container {
                padding: 10px;
            }
            
            .card {
                padding: 20px;
            }
            
            .image-info {
                flex-direction: column;
                gap: 15px;
            }
            
            .action-buttons {
                flex-direction: column;
                align-items: center;
            }
            
            .btn {
                width: 100%;
                max-width: 300px;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>智能图片切割工具</h1>
            <p class="subtitle">上传长图,自动切割为多个不超过5MB的片段,保持原始宽度不变,尽可能减少切割数量</p>
        </header>
        
        <main>
            <div class="card">
                <div id="upload-section">
                    <div class="upload-area" id="drop-area">
                        <i>📁</i>
                        <h2>上传您的图片</h2>
                        <p>点击或拖放图片到此处<br>支持JPG、PNG等格式,图片大小无限制</p>
                        <button class="btn" id="select-btn">选择图片</button>
                        <input type="file" id="file-input" accept="image/*">
                    </div>
                </div>
                
                <div class="loading" id="loading">
                    <div class="spinner"></div>
                    <p id="status-text">处理中,请稍候...</p>
                    <div class="progress-container">
                        <div class="progress-bar" id="progress-bar"></div>
                    </div>
                </div>
                
                <div class="preview-container" id="preview-section">
                    <div class="image-info">
                        <div>
                            <h3>原始图片尺寸</h3>
                            <p id="original-dimensions">-</p>
                        </div>
                        <div>
                            <h3>原始文件大小</h3>
                            <p id="original-size">-</p>
                        </div>
                        <div>
                            <h3>切割片段数量</h3>
                            <p id="slice-count">-</p>
                        </div>
                    </div>
                    
                    <div class="action-buttons">
                        <button class="btn" id="process-btn">开始切割图片</button>
                        <button class="btn btn-secondary" id="reset-btn">重新选择图片</button>
                    </div>
                </div>
            </div>
            
            <div class="card results-container" id="results-section">
                <div class="results-header">
                    <h2>切割结果</h2>
                    <button class="btn" id="download-all">下载全部图片</button>
                </div>
                
                <div class="slices-container" id="slices-container">
                    <!-- 切割后的图片将在这里展示 -->
                </div>
            </div>
        </main>
        
        <footer>
            <p>© 2023 智能图片切割工具 | 使用Canvas技术实现 | 所有图片处理均在浏览器完成</p>
        </footer>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', function() {
            // 获取DOM元素
            const fileInput = document.getElementById('file-input');
            const selectBtn = document.getElementById('select-btn');
            const dropArea = document.getElementById('drop-area');
            const processBtn = document.getElementById('process-btn');
            const resetBtn = document.getElementById('reset-btn');
            const downloadAllBtn = document.getElementById('download-all');
            const previewSection = document.getElementById('preview-section');
            const resultsSection = document.getElementById('results-section');
            const loadingSection = document.getElementById('loading');
            const statusText = document.getElementById('status-text');
            const progressBar = document.getElementById('progress-bar');
            const slicesContainer = document.getElementById('slices-container');
            
            // 存储原始图片和切割结果
            let originalImage = null;
            let imageSlices = [];
            
            // 事件绑定
            selectBtn.addEventListener('click', () => fileInput.click());
            fileInput.addEventListener('change', handleFileSelect);
            dropArea.addEventListener('dragover', handleDragOver);
            dropArea.addEventListener('drop', handleDrop);
            processBtn.addEventListener('click', processImage);
            resetBtn.addEventListener('click', resetApp);
            downloadAllBtn.addEventListener('click', downloadAllSlices);
            
            // 处理文件选择
            function handleFileSelect(e) {
                const file = e.target.files[0];
                if (file && file.type.match('image.*')) {
                    loadImage(file);
                }
            }
            
            // 处理拖放
            function handleDragOver(e) {
                e.preventDefault();
                e.stopPropagation();
                dropArea.style.borderColor = 'rgba(255, 255, 255, 0.7)';
                dropArea.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
            }
            
            function handleDrop(e) {
                e.preventDefault();
                e.stopPropagation();
                dropArea.style.borderColor = 'rgba(255, 255, 255, 0.4)';
                dropArea.style.backgroundColor = '';
                
                const file = e.dataTransfer.files[0];
                if (file && file.type.match('image.*')) {
                    loadImage(file);
                }
            }
            
            // 加载图片
            function loadImage(file) {
                const reader = new FileReader();
                
                reader.onload = function(e) {
                    const img = new Image();
                    img.onload = function() {
                        originalImage = {
                            element: img,
                            file: file,
                            width: img.width,
                            height: img.height,
                            size: file.size
                        };
                        
                        // 显示图片信息
                        document.getElementById('original-dimensions').textContent = 
                            `${img.width} × ${img.height} 像素`;
                        document.getElementById('original-size').textContent = 
                            formatFileSize(file.size);
                        document.getElementById('slice-count').textContent = 
                            calculateSliceCount(img.height, file.size);
                        
                        // 显示预览区域
                        previewSection.style.display = 'block';
                        resultsSection.style.display = 'none';
                    };
                    img.src = e.target.result;
                };
                
                reader.readAsDataURL(file);
            }
            
            // 计算切割数量
            function calculateSliceCount(height, fileSize) {
                // 简单估算:假设文件大小与高度成正比
                const maxSize = 5 * 1024 * 1024; // 5MB
                const minSlices = Math.ceil(fileSize / maxSize);
                
                // 高度切割的最小单位(至少保留100像素高度)
                const minHeight = 100;
                const maxSliceHeight = Math.max(minHeight, Math.floor(height / minSlices));
                
                // 实际切片数
                const sliceCount = Math.ceil(height / maxSliceHeight);
                return sliceCount;
            }
            
            // 处理图片切割
            async function processImage() {
                if (!originalImage) return;
                
                // 显示加载状态
                loadingSection.style.display = 'block';
                previewSection.style.display = 'none';
                resultsSection.style.display = 'none';
                
                // 清除之前的切割结果
                imageSlices = [];
                slicesContainer.innerHTML = '';
                
                try {
                    // 计算切割参数
                    const maxSize = 4.8 * 1024 * 1024; // 4.8MB (预留空间)
                    const originalWidth = originalImage.width;
                    const originalHeight = originalImage.height;
                    
                    // 初始切片高度(基于原始大小比例)
                    let sliceHeight = Math.floor(originalHeight * (maxSize / originalImage.size));
                    
                    // 确保最小高度
                    sliceHeight = Math.max(100, sliceHeight);
                    
                    // 计算切片数量
                    const sliceCount = Math.ceil(originalHeight / sliceHeight);
                    
                    // 更新状态
                    statusText.textContent = `准备切割图片为 ${sliceCount} 个片段...`;
                    progressBar.style.width = '0%';
                    
                    // 创建临时canvas
                    const canvas = document.createElement('canvas');
                    const ctx = canvas.getContext('2d');
                    canvas.width = originalWidth;
                    
                    // 处理每个切片
                    for (let i = 0; i < sliceCount; i++) {
                        // 更新进度
                        const progress = Math.floor((i / sliceCount) * 100);
                        progressBar.style.width = `${progress}%`;
                        statusText.textContent = `处理片段 ${i+1}/${sliceCount}...`;
                        
                        // 计算当前切片的y坐标和高度
                        const y = i * sliceHeight;
                        const h = Math.min(sliceHeight, originalHeight - y);
                        
                        // 设置canvas高度
                        canvas.height = h;
                        
                        // 清除画布
                        ctx.clearRect(0, 0, canvas.width, canvas.height);
                        
                        // 绘制切片
                        ctx.drawImage(
                            originalImage.element, 
                            0, y, originalWidth, h,
                            0, 0, originalWidth, h
                        );
                        
                        // 获取Blob并调整质量
                        let quality = 0.9;
                        let blob = null;
                        
                        do {
                            // 将canvas转为Blob
                            blob = await new Promise(resolve => 
                                canvas.toBlob(resolve, 'image/jpeg', quality)
                            );
                            
                            // 如果文件太大,降低质量
                            if (blob.size > maxSize && quality > 0.5) {
                                quality -= 0.1;
                            } else {
                                break;
                            }
                        } while (quality > 0.5);
                        
                        // 创建切片对象
                        const slice = {
                            index: i,
                            width: originalWidth,
                            height: h,
                            blob: blob,
                            url: URL.createObjectURL(blob),
                            size: blob.size
                        };
                        
                        imageSlices.push(slice);
                    }
                    
                    // 显示结果
                    showResults();
                    
                } catch (error) {
                    console.error('图片处理失败:', error);
                    statusText.textContent = '处理失败,请重试';
                } finally {
                    // 隐藏加载状态
                    loadingSection.style.display = 'none';
                }
            }
            
            // 显示切割结果
            function showResults() {
                // 更新切片数量显示
                document.getElementById('slice-count').textContent = imageSlices.length;
                
                // 显示结果区域
                resultsSection.style.display = 'block';
                previewSection.style.display = 'block';
                
                // 清空容器
                slicesContainer.innerHTML = '';
                
                // 添加每个切片
                imageSlices.forEach((slice, index) => {
                    const sliceElement = document.createElement('div');
                    sliceElement.className = 'slice-card';
                    sliceElement.innerHTML = `
                        <img src="${slice.url}" alt="图片片段 ${index+1}" class="slice-img">
                        <div class="slice-info">
                            <div class="slice-name">片段 #${index+1}</div>
                            <div class="slice-size">${formatFileSize(slice.size)} | ${slice.width}×${slice.height}</div>
                        </div>
                    `;
                    
                    // 添加点击下载事件
                    sliceElement.addEventListener('click', () => {
                        downloadSlice(slice, index);
                    });
                    
                    slicesContainer.appendChild(sliceElement);
                });
            }
            
            // 下载单个切片
            function downloadSlice(slice, index) {
                const a = document.createElement('a');
                a.href = slice.url;
                a.download = `image-slice-${index+1}.jpg`;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
            }
            
            // 下载所有切片
            function downloadAllSlices() {
                imageSlices.forEach((slice, index) => {
                    setTimeout(() => {
                        downloadSlice(slice, index);
                    }, index * 300); // 避免同时下载导致浏览器阻塞
                });
            }
            
            // 重置应用
            function resetApp() {
                // 清除所有状态
                originalImage = null;
                imageSlices = [];
                
                // 释放URL对象
                imageSlices.forEach(slice => URL.revokeObjectURL(slice.url));
                
                // 重置UI
                fileInput.value = '';
                previewSection.style.display = 'none';
                resultsSection.style.display = 'none';
                slicesContainer.innerHTML = '';
            }
            
            // 辅助函数:格式化文件大小
            function formatFileSize(bytes) {
                if (bytes < 1024) return bytes + ' B';
                else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
                else return (bytes / 1048576).toFixed(1) + ' MB';
            }
        });
    </script>
</body>
</html>