You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修改JavaScript代码实现多文件夹批量图像处理并按目录输出?

解决多文件夹批量图像处理问题

问题根源

你的代码核心逻辑框架没问题,但存在3个关键问题导致多文件夹处理失效:

  1. 路径分隔符不兼容:仅处理了/分隔符,Windows系统下文件路径使用\,导致无法正确区分不同文件夹。
  2. 无错误处理机制:FileReader、图片加载、压缩环节未捕获错误,单个文件失败会导致Promise永久pending,批量处理卡住。
  3. 回调式压缩函数难以管理: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 &amp; 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>

关键修改点说明

  1. 路径处理优化:用split(/[\\/]/)分割路径,同时支持Windows和Unix系统的路径分隔符,确保多文件夹能被正确识别。
  2. 全链路错误捕获:为FileReader、图片加载、图像处理、压缩环节添加错误捕获,任何环节失败都会触发Promise reject,避免批量处理卡住。
  3. 压缩函数Promise化:将compressImage改为返回Promise,简化异步流程,便于整合到Promise链中统一管理。
  4. 独立Promise封装:每个文件的处理逻辑都封装在独立的Promise中,确保单个文件失败不会影响其他文件的处理。

内容的提问来源于stack exchange,提问作者Blooberz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 08:22:02