如何修改JavaScript代码实现多文件夹批量图像处理并按目录输出?
解决多文件夹批量图像处理问题
问题根源
你的代码核心逻辑框架没问题,但存在3个关键问题导致多文件夹处理失效:
- 路径分隔符不兼容:仅处理了
/分隔符,Windows系统下文件路径使用\,导致无法正确区分不同文件夹。 - 无错误处理机制:FileReader、图片加载、压缩环节未捕获错误,单个文件失败会导致Promise永久pending,批量处理卡住。
- 回调式压缩函数难以管理:
compressImage用回调实现,无法无缝整合到Promise链中,增加异步流程复杂度。
修改方案及完整代码
修改后的JavaScript代码
document.addEventListener('DOMContentLoaded', function() { const folderInput = document.getElementById('folderInput'); folderInput.addEventListener('change', function() { updateSelectedFilesPreview('folderInput', 'bulk_selectedFilesPreview'); console.log('Folder input changed. Files selected:', folderInput.files.length); }); }); function processFolderImages() { const folderInput = document.getElementById('folderInput'); const output = document.getElementById('bulk_output'); const finalSize = 2000; const maxScaleSize = 1900; const compressionQuality = 0.5; const zip = new JSZip(); const promises = []; // 移除旧下载按钮 const oldButton = document.querySelector('.download-bulkimg-button'); if (oldButton) { output.removeChild(oldButton); } const folderMap = {}; // 遍历文件,按文件夹分类(兼容/和\分隔符) for (let i = 0; i < folderInput.files.length; i++) { const file = folderInput.files[i]; const filePath = file.webkitRelativePath || file.relativePath || file.name; // 用正则分割路径,同时支持Windows和Unix风格分隔符 const pathParts = filePath.split(/[\\/]/); const folderPath = pathParts.slice(0, -1).join('/'); // 拼接回/分隔的路径 console.log(`Processing file: ${file.name}`); console.log(`File path: ${filePath}`); console.log(`Folder path: ${folderPath}`); if (!folderMap[folderPath]) { folderMap[folderPath] = []; } folderMap[folderPath].push(file); } console.log('Folder map:', folderMap); // 处理每个文件夹的文件 for (const folderPath in folderMap) { const files = folderMap[folderPath]; const folder = zip.folder(folderPath); console.log(`Processing folder: ${folderPath}`); files.forEach(file => { const processPromise = new Promise((resolve, reject) => { const reader = new FileReader(); // 文件读取成功 reader.onload = function(e) { console.log(`Reading file: ${file.name}`); const img = new Image(); // 图片加载成功 img.onload = function() { console.log(`Image loaded: ${file.name}`); try { // 裁剪处理 const cropCanvas = document.createElement('canvas'); const cropCtx = cropCanvas.getContext('2d'); const { left, top, width, height } = getBoundingRectangle(img); cropCanvas.width = width; cropCanvas.height = height; cropCtx.drawImage(img, left, top, width, height, 0, 0, width, height); // 缩放处理 const scaleCanvas = document.createElement('canvas'); const scaleCtx = scaleCanvas.getContext('2d'); let scaleWidth, scaleHeight; if (width > height) { scaleWidth = maxScaleSize; scaleHeight = (height / width) * maxScaleSize; } else { scaleWidth = (width / height) * maxScaleSize; scaleHeight = maxScaleSize; } scaleCanvas.width = scaleWidth; scaleCanvas.height = scaleHeight; scaleCtx.drawImage(cropCanvas, 0, 0, width, height, 0, 0, scaleWidth, scaleHeight); // 调整尺寸到固定画布 const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const canvasSize = finalSize; canvas.width = canvasSize; canvas.height = canvasSize; let sourceWidth, sourceHeight; if (scaleWidth > scaleHeight) { sourceWidth = canvasSize; sourceHeight = (scaleHeight / scaleWidth) * canvasSize; } else { sourceWidth = (scaleWidth / scaleHeight) * canvasSize; sourceHeight = canvasSize; } const xOffset = (canvasSize - sourceWidth) / 2; const yOffset = (canvasSize - sourceHeight) / 2; ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canvasSize, canvasSize); ctx.drawImage(scaleCanvas, 0, 0, scaleWidth, scaleHeight, xOffset, yOffset, sourceWidth, sourceHeight); // 转换为Blob并压缩 canvas.toBlob(function(blob) { console.log(`Image processing complete: ${file.name}`); // 使用Promise版本的压缩函数 compressImage(blob, compressionQuality) .then(compressedBlob => { folder.file(file.name, compressedBlob); resolve(); }) .catch(compressError => { console.error(`Compression failed for ${file.name}:`, compressError); reject(compressError); }); }, 'image/jpeg', 1); } catch (processError) { console.error(`Processing failed for ${file.name}:`, processError); reject(processError); } }; // 图片加载失败 img.onerror = function() { const error = new Error(`Failed to load image: ${file.name}`); console.error(error); reject(error); }; img.src = e.target.result; }; // 文件读取失败 reader.onerror = function() { const error = new Error(`Failed to read file: ${file.name}`); console.error(error, reader.error); reject(error); }; reader.readAsDataURL(file); }); promises.push(processPromise); }); } // 等待所有处理完成,生成ZIP Promise.all(promises).then(() => { console.log('All images processed. Generating ZIP...'); if (!document.querySelector('.download-folder-img-button')) { zip.generateAsync({ type: 'blob' }) .then(content => { console.log('ZIP generated.'); const button = document.createElement('button'); button.innerText = 'Download'; button.classList.add('download-bulkimg-button'); button.addEventListener('click', function(event) { event.preventDefault(); const a = document.createElement('a'); a.href = URL.createObjectURL(content); a.download = 'processed-images.zip'; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); }); output.appendChild(button); }) .catch(error => { console.error('Error generating ZIP:', error); output.innerHTML = 'An error occurred while generating the ZIP.'; }); } }).catch(error => { console.error('Error processing images:', error); output.innerHTML = `An error occurred during processing: ${error.message}`; }); } function getBoundingRectangle(img) { const offscreenCanvas = document.createElement('canvas'); const offscreenCtx = offscreenCanvas.getContext('2d'); offscreenCanvas.width = img.width; offscreenCanvas.height = img.height; offscreenCtx.drawImage(img, 0, 0); const imageData = offscreenCtx.getImageData(0, 0, offscreenCanvas.width, offscreenCanvas.height); const data = imageData.data; let left = offscreenCanvas.width; let top = offscreenCanvas.height; let right = 0; let bottom = 0; for (let y = 0; y < offscreenCanvas.height; y++) { for (let x = 0; x < offscreenCanvas.width; x++) { const index = (y * offscreenCanvas.width + x) * 4; if (data[index] !== 255 || data[index + 1] !== 255 || data[index + 2] !== 255) { left = Math.min(left, x); right = Math.max(right, x); top = Math.min(top, y); bottom = Math.max(bottom, y); } } } const width = right - left; const height = bottom - top; console.log(`Bounding rectangle - left: ${left}, top: ${top}, width: ${width}, height: ${height}`); return { left, top, width, height }; } // 改为Promise版本的压缩函数 function compressImage(blob, quality) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = function() { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); canvas.toBlob(function(compressedBlob) { if (!compressedBlob) { reject(new Error('Failed to compress image')); return; } console.log('Image compressed.'); resolve(compressedBlob); }, 'image/jpeg', quality); }; img.onerror = function() { reject(new Error('Failed to load blob for compression')); }; img.src = URL.createObjectURL(blob); }); }
HTML代码(无需修改)
<div id="bulkContent" class="tab-content"> <div class="imageContent-centrecontainer" style="margin-left: 150px;"> <p class="header-card">Bulk Image Tool</p> <div class="imagecontent-container"> <div class="imageform-container"> <form> <div class="custom-input-container"> <label for="folderInput" class="custom-button"> <i class="fa-solid fa-cloud-arrow-down" style="display: grid; justify-content: center;"></i> Drag & Drop Or Select </label> <div id="fileList" class="file-list"></div> <input type="file" id="folderInput" class="custom-input" webkitdirectory multiple> </div> <button class="custom-button-process" type="button" onclick="processFolderImages()">Process Images</button> <div id="bulk_output"></div> </form> </div> </div> </div> <div class="imgdisplay-container"> <div class="imgpreview-container" id="dragAndDropArea"> <div id="bulk_selectedFilesPreview"> </div> </div> </div> </div>
关键修改点说明
- 路径处理优化:用
split(/[\\/]/)分割路径,同时支持Windows和Unix系统的路径分隔符,确保多文件夹能被正确识别。 - 全链路错误捕获:为FileReader、图片加载、图像处理、压缩环节添加错误捕获,任何环节失败都会触发Promise reject,避免批量处理卡住。
- 压缩函数Promise化:将
compressImage改为返回Promise,简化异步流程,便于整合到Promise链中统一管理。 - 独立Promise封装:每个文件的处理逻辑都封装在独立的Promise中,确保单个文件失败不会影响其他文件的处理。
内容的提问来源于stack exchange,提问作者Blooberz
相关产品推荐
相关产品推荐

