< Project-37 PicsAdjTool> 照片白平衡批处理调整 可以批量裁剪工具 Ver.0.3 -- Last updated on 15Mar.2025 添加剪切全部应用功能

creator Dave on 12Mar.2025

更新:12Mar.2025

  • 添加剪切功能,支持批量剪切,批量下载。
  • 剪辑的大小是 600x600psi
  • 剪切后,预览图框也同步变为正文形。
  • CSS JS HTML 为独立文件。
  • 色温与色调用 添加颜色解释
  • 色温改为 K ,100K 间隔。

更新:15Mar.2025

  • 修改了多个BUGs
  • 添加 “应用裁剪到所有照片" 功能
  • javascript 文件从 scrip.js 按功能分为了两个: main.js image-editor.js
  • docker 部署
  • 在文章”完整代码” 中添加:目录结构、app.py、Dockerfile、requirements.txt 代码与 docker 命令 补全了 docker 部署的代码。
  • 完全复制代码,并按给定文件名保存,可以在支持 flask 环境下运行。
  • 项目名字更新为 PicsAdjTool ,旧名 BWB
  • Claude AI Modified codes
  • !!!完整代码中是以 container 中运行的应用,不再是浏览器版本!!!
  • 如果只想在浏览器中直接运行,见附件 1 纯浏览器运行,注意文件结构,并用在根目录创建index.html 含代码即可。 

演示如下:

起因:

拍了几组绿豆照片,前三行是在屋子后面拍的,已经删掉了22张。 后面是在屋子前面拍的。一组偏蓝,一组偏黄(夕阳)

我买的软件是 ACDsee 2019 但这个不支持批量操作:

Photoshop 现在是每年付费,就为这点事儿不值得,就我这速度,2月未必有多大进展,算了。

 在网上看到 JS 可以做到,抄还不会吗?在 AI 帮助下,于是:

PicsAdjTool

功能:

  • 多图片从目录或拖拽上传
  • 图片缩略图预览与选择
  • 工作区显示原图与调整后图像
  • 可以对图片的色温、色调、亮度、对比度、饱和度 做调整
  • 可以白点取样(我没有色板,没试过,但可以工作)
  • 调整后可以批量执行,也可以单照片修改

完整代码:

目录结构:

/                          
├── app.py                
├── templates/             
│   └── index.html         
└── static/            
    ├── css/              
    │   └── styles.css    
    └── js/                
        ├── image-editor.js
        └── main.js        

styles.css

body {
    font-family: Arial, sans-serif;
    max-width: 1000px;
    margin: 0 auto;
    padding: 20px;
    background-color: #f5f5f5;
  }
  h1 {
    text-align: center;
    color: #2e7d32;
  }
  .container {
    display: flex;
    flex-direction: column;
    gap: 20px;
    background-color: white;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
  }
  .upload-section {
    border: 2px dashed #ccc;
    padding: 20px;
    text-align: center;
    cursor: pointer;
    border-radius: 4px;
    transition: border-color 0.3s;
  }
  .upload-section:hover {
    border-color: #2e7d32;
  }
  .controls {
    display: flex;
    flex-wrap: wrap;
    gap: 15px;
    margin-bottom: 20px;
  }
  .control-group {
    flex: 1;
    min-width: 200px;
  }
  .preview-section {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
  }
  .preview-container {
    flex: 1;
    min-width: 300px;
    position: relative;
  }
  .preview-container h3 {
    text-align: center;
    margin-top: 0;
  }
  canvas {
    width: 100%;
    height: auto;
    max-height: 500px;
    object-fit: contain;
    border: 1px solid #ddd;
    border-radius: 4px;
  }
  .button-group {
    display: flex;
    justify-content: center;
    gap: 10px;
    margin-top: 20px;
  }
  button {
    background-color: #2e7d32;
    color: white;
    border: none;
    padding: 10px 15px;
    border-radius: 4px;
    cursor: pointer;
    font-size: 16px;
    transition: background-color 0.3s;
  }
  button:hover {
    background-color: #1b5e20;
  }
  button:disabled {
    background-color: #ccc;
    cursor: not-allowed;
  }
  .slider-container {
    margin: 10px 0;
  }
  label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
  }
  input[type="range"] {
    width: 100%;
  }
  .value-display {
    display: inline-block;
    width: 60px;
    text-align: right;
  }
  .picker-container {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 10px 0;
  }
  .color-preview {
    width: 20px;
    height: 20px;
    border: 1px solid #ccc;
    border-radius: 3px;
  }
  .thumbnail-container {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 20px;
  }
  .thumbnail {
    width: 100px;
    height: 100px;
    object-fit: cover;
    border: 2px solid transparent;
    border-radius: 4px;
    cursor: pointer;
    transition: border-color 0.3s;
  }
  .thumbnail.selected {
    border-color: #2e7d32;
  }
  .progress-bar {
    height: 10px;
    background-color: #e0e0e0;
    border-radius: 5px;
    margin-top: 10px;
    overflow: hidden;
  }
  .progress {
    height: 100%;
    background-color: #2e7d32;
    width: 0%;
    transition: width 0.3s;
  }
  .hidden {
    display: none;
  }
  #info-text {
    text-align: center;
    margin: 10px 0;
    font-style: italic;
    color: #666;
  }
  .eyedropper {
    cursor: crosshair;
  }
  .help-text {
    color: #666;
    font-size: 14px;
    margin-top: 5px;
  }
  .crop-container {
    position: relative;
    margin: 15px 0;
    border: 1px solid #ddd;
    border-radius: 4px;
    overflow: hidden;
  }
  .crop-overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.5);
    pointer-events: none;
  }
  .crop-area {
    position: absolute;
    border: 2px dashed #fff;
    box-sizing: border-box;
    pointer-events: none;
  }
  .crop-handle {
    position: absolute;
    width: 10px;
    height: 10px;
    background-color: #fff;
    border: 1px solid #2e7d32;
    border-radius: 2px;
    cursor: move;
  }
  .crop-image {
    display: block;
    max-width: 100%;
    max-height: 600px;
    cursor: move;
  }
  .temp-scale {
    display: flex;
    justify-content: space-between;
    margin-top: 3px;
    font-size: 12px;
    color: #666;
  }
  .tint-scale {
    display: flex;
    justify-content: space-between;
    margin-top: 3px;
    font-size: 12px;
    color: #666;
  }
  .color-indicator {
    height: 5px;
    margin-top: 2px;
    border-radius: 2px;
    background: linear-gradient(to right, #4d88ff, #ffffff, #ff7043);
  }
  .tint-indicator {
    height: 5px;
    margin-top: 2px;
    border-radius: 2px;
    background: linear-gradient(to right, #ab47bc, #ffffff, #4caf50);
  }
  .slider-container {
    margin: 10px 0;
  }
  
  #apply-crop-all {
    margin-top: 10px;
    background-color: #1b5e20;
  }

index.html

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>芽豆(绿豆)照片白平衡调整工具</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
</head>
<body>
  <h1>芽豆(绿豆)照片白平衡调整工具</h1>
  <div class="container">
    <div id="upload-section" class="upload-section" onclick="document.getElementById('file-input').click();">
      <p>点击或拖放照片到此处</p>
      <p><small>支持多张照片同时上传,每张建议不超过10MB</small></p>
      <input type="file" id="file-input" multiple accept="image/*" style="display: none;" onchange="handleFilesFromInput(this.files)">
      <div class="progress-bar hidden" id="progress-bar">
        <div class="progress" id="progress"></div>
      </div>
    </div>
    
    <div id="info-text">上传照片后即可进行白平衡调整</div>
    
    <div id="thumbnails" class="thumbnail-container hidden"></div>
    
    <div id="editor" class="hidden">
      <div class="controls">
        <div class="control-group">
          <h3>白平衡调整</h3>
          
          <div class="picker-container">
            <label>白点取样:</label>
            <button id="white-picker">选择白点</button>
            <div class="color-preview" id="white-preview"></div>
            <div class="help-text">在图像中点击应该是白色的区域</div>
          </div>
          
          <div class="slider-container">
            <label>色温 (K): <span class="value-display" id="temp-value">5500K</span></label>
            <input type="range" id="temp-slider" min="2500" max="10000" value="5500" step="100">
            <div class="color-indicator"></div>
            <div class="temp-scale">
              <span>冷色调 (蓝)</span>
              <span>暖色调 (黄)</span>
            </div>
          </div>
          
          <div class="slider-container">
            <label>色调: <span class="value-display" id="tint-value">0</span></label>
            <input type="range" id="tint-slider" min="-100" max="100" value="0">
            <div class="tint-indicator"></div>
            <div class="tint-scale">
              <span>紫</span>
              <span>绿</span>
            </div>
          </div>
        </div>
        
        <div class="control-group">
          <div class="control-group">
            <h3>裁剪工具</h3>
            <div class="slider-container">
              <button id="enable-crop">启用裁剪 (600×600)</button>
              <button id="apply-crop" class="hidden">应用裁剪</button>
              <button id="cancel-crop" class="hidden">取消裁剪</button>
              <button id="reset-crop" class="hidden">重置裁剪</button>
              <button id="apply-crop-all">应用裁剪到所有照片</button>
              <div class="help-text">裁剪为固定600×600像素区域,拖动图像定位裁剪区域</div>
            </div>
          
          <h3>曝光调整</h3>
          <div class="slider-container">
            <label>亮度: <span class="value-display" id="brightness-value">0</span></label>
            <input type="range" id="brightness-slider" min="-100" max="100" value="0">
          </div>
          
          <div class="slider-container">
            <label>对比度: <span class="value-display" id="contrast-value">0</span></label>
            <input type="range" id="contrast-slider" min="-100" max="100" value="0">
          </div>
          
          <div class="slider-container">
            <label>饱和度: <span class="value-display" id="saturation-value">0</span></label>
            <input type="range" id="saturation-slider" min="-100" max="100" value="0">
          </div>
        </div>
      </div>
      
      <div class="preview-section">
        <div class="preview-container">
          <h3>原图</h3>
          <div id="crop-container" class="crop-container hidden">
            <img id="crop-image" class="crop-image">
            <div class="crop-overlay"></div>
            <div id="crop-area" class="crop-area"></div>
          </div>
          <canvas id="original-canvas"></canvas>
        </div>
        <div class="preview-container">
          <h3>调整后</h3>
          <canvas id="edited-canvas"></canvas>
        </div>
      </div>
      
      <div class="button-group">
        <button id="apply-all">应用到所有照片</button>
        <button id="reset-button">重置调整</button>
        <button id="download-button">下载调整后的照片</button>
        <button id="download-all-button">下载所有调整后的照片</button>
      </div>
    </div>
  </div>

  <script src="{{ url_for('static', filename='js/image-editor.js') }}"></script>
  <script src="{{ url_for('static', filename='js/main.js') }}"></script>
  <script>
    // 直接处理文件输入的函数
    function handleFilesFromInput(files) {
      console.log("从HTML触发文件处理:", files.length, "个文件");
      if (window.handleFiles) {
        window.handleFiles(files);
      } else {
        console.error("找不到handleFiles函数");
        alert("上传功能初始化失败,请刷新页面重试");
      }
    }
  </script>
</body>
</html>

main.js

// 主程序
document.addEventListener('DOMContentLoaded', function() {
    // 变量声明
    const uploadSection = document.getElementById('upload-section');
    const fileInput = document.getElementById('file-input');
    const progressBar = document.getElementById('progress-bar');
    const progress = document.getElementById('progress');
    const thumbnailsContainer = document.getElementById('thumbnails');
    const editor = document.getElementById('editor');
    const infoText = document.getElementById('info-text');
    const originalCanvas = document.getElementById('original-canvas');
    const editedCanvas = document.getElementById('edited-canvas');
    const tempSlider = document.getElementById('temp-slider');
    const tintSlider = document.getElementById('tint-slider');
    const brightnessSlider = document.getElementById('brightness-slider');
    const contrastSlider = document.getElementById('contrast-slider');
    const saturationSlider = document.getElementById('saturation-slider');
    const tempValue = document.getElementById('temp-value');
    const tintValue = document.getElementById('tint-value');
    const brightnessValue = document.getElementById('brightness-value');
    const contrastValue = document.getElementById('contrast-value');
    const saturationValue = document.getElementById('saturation-value');
    const resetButton = document.getElementById('reset-button');
    const downloadButton = document.getElementById('download-button');
    const downloadAllButton = document.getElementById('download-all-button');
    const applyAllButton = document.getElementById('apply-all');
    const whitePicker = document.getElementById('white-picker');
    const whitePreview = document.getElementById('white-preview');
    
    // 裁剪相关元素
    const enableCropButton = document.getElementById('enable-crop');
    const applyCropButton = document.getElementById('apply-crop');
    const cancelCropButton = document.getElementById('cancel-crop');
    const resetCropButton = document.getElementById('reset-crop');
    const applyCropAllButton = document.getElementById('apply-crop-all');
    const cropContainer = document.getElementById('crop-container');
    const cropImage = document.getElementById('crop-image');
    const cropArea = document.getElementById('crop-area');
  
    // 存储图像数据和编辑设置 - 使用全局变量以便内联脚本访问
    window.images = [];
    window.currentImageIndex = -1;
    let originalCtx, editedCtx;
    let isWhitePickerActive = false;
    window.isCropMode = false;
    let isDragging = false;
    let startX = 0;
    let startY = 0;
    let initialCropX = 0;
    let initialCropY = 0;
    let cropX = 0;
    let cropY = 0;
    
    // 默认设置
    const defaultSettings = {
      temp: 5500,  // 默认色温值 (K)
      tint: 0,     // 色调(紫-绿)
      brightness: 0,
      contrast: 0,
      saturation: 0,
      cropX: null, // 裁剪X起点
      cropY: null, // 裁剪Y起点
      isCropped: false // 是否已裁剪
    };
    
    // 当前设置
    let currentSettings = { ...defaultSettings };
  
    console.log("初始化上传功能...");
    console.log("上传区域元素:", uploadSection ? "已找到" : "未找到");
    console.log("文件输入元素:", fileInput ? "已找到" : "未找到");
    
    // 从imageEditor获取函数,确保在使用前已定义
    let renderImage, applyAdjustments, calculateWhiteBalance;
    
    if (window.imageEditor) {
      renderImage = window.imageEditor.renderImage;
      applyAdjustments = window.imageEditor.applyAdjustments;
      calculateWhiteBalance = window.imageEditor.calculateWhiteBalance;
      
      // 设置下载按钮事件
      if (downloadButton) {
        downloadButton.addEventListener('click', window.imageEditor.downloadImage);
      }
      
      if (downloadAllButton) {
        downloadAllButton.addEventListener('click', window.imageEditor.downloadAllImages);
      }
      
      if (applyAllButton) {
        applyAllButton.addEventListener('click', window.imageEditor.applySettingsToAll);
      }
    } else {
      console.error("imageEditor模块未找到,核心功能将不可用");
      // 定义临时空函数,避免报错
      renderImage = function() {
        console.error("renderImage函数未定义");
      };
      applyAdjustments = function() {
        console.error("applyAdjustments函数未定义");
      };
      calculateWhiteBalance = function() {
        console.error("calculateWhiteBalance函数未定义");
      };
    }
    
    // =====================================================================
    // 文件上传和处理功能
    // =====================================================================
    
    // 设置上传区域事件 - 确保事件正确绑定
    if (uploadSection) {
        uploadSection.onclick = function(e) {
            console.log("上传区域被点击");
            if (fileInput) {
                fileInput.click();
            } else {
                console.error("找不到文件输入元素!");
            }
        };
        
        uploadSection.ondragover = function(e) {
            e.preventDefault();
            e.stopPropagation();
            uploadSection.style.borderColor = '#2e7d32';
            console.log("文件拖动到上传区域上方");
        };
        
        uploadSection.ondragleave = function(e) {
            e.preventDefault();
            e.stopPropagation();
            uploadSection.style.borderColor = '#ccc';
            console.log("文件离开上传区域");
        };
        
        uploadSection.ondrop = function(e) {
            console.log("文件被拖放到上传区域");
            e.preventDefault();
            e.stopPropagation();
            uploadSection.style.borderColor = '#ccc';
            
            if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
                console.log("检测到", e.dataTransfer.files.length, "个文件");
                handleFiles(e.dataTransfer.files);
            } else {
                console.error("未检测到文件或文件列表为空");
            }
        };
    } else {
        console.error("找不到上传区域元素!");
    }
    
    // 文件输入事件
    if (fileInput) {
      fileInput.onchange = function(e) {
          console.log("文件输入发生变化");
          if (this.files && this.files.length > 0) {
              console.log("选择了", this.files.length, "个文件");
              handleFiles(this.files);
          } else {
              console.error("未选择文件或文件列表为空");
          }
      };
    } else {
      console.error("找不到文件输入元素!");
    }
    
    // 处理上传的文件
    function handleFiles(files) {
      try {
          console.log("开始处理文件...");
          progressBar.classList.remove('hidden');
          window.images = [];
          thumbnailsContainer.innerHTML = '';
          window.currentImageIndex = -1;
          
          const totalFiles = files.length;
          let loadedFiles = 0;
          let imageFiles = 0;
          
          console.log("总文件数:", totalFiles);
          
          // 先检查有多少个图像文件
          for (let i = 0; i < files.length; i++) {
              if (files[i].type.startsWith('image/')) {
                  imageFiles++;
              } else {
                  console.warn("文件不是图像:", files[i].name, files[i].type);
              }
          }
          
          if (imageFiles === 0) {
              console.error("没有可处理的图像文件");
              progressBar.classList.add('hidden');
              alert("请选择图像文件 (JPG, PNG, GIF等)");
              return;
          }
          
          console.log("有效图像文件数:", imageFiles);
          
          // 处理每个文件
          Array.from(files).forEach((file, index) => {
              if (!file.type.startsWith('image/')) {
                  console.log("跳过非图像文件:", file.name);
                  return;
              }
              
              console.log("处理图像文件:", file.name, "类型:", file.type);
              
              const reader = new FileReader();
              
              reader.onerror = function(error) {
                  console.error("读取文件时出错:", error);
                  loadedFiles++;
                  progress.style.width = (loadedFiles / imageFiles * 100) + '%';
                  
                  if (loadedFiles === imageFiles) {
                      finishLoading();
                  }
              };
              
              reader.onload = function(e) {
                  console.log("文件读取完成:", file.name);
                  
                  const img = new Image();
                  
                  img.onerror = function() {
                      console.error("图像加载失败:", file.name);
                      loadedFiles++;
                      progress.style.width = (loadedFiles / imageFiles * 100) + '%';
                      
                      if (loadedFiles === imageFiles) {
                          finishLoading();
                      }
                  };
                  
                  img.onload = function() {
                      console.log("图像加载成功:", file.name, img.width, "x", img.height);
                      
                      window.images.push({
                          img: img,
                          name: file.name,
                          settings: { ...defaultSettings },
                          isEdited: false // 标记是否已编辑
                      });
                      
                      createThumbnail(img, window.images.length - 1);
                      
                      loadedFiles++;
                      progress.style.width = (loadedFiles / imageFiles * 100) + '%';
                      
                      console.log("进度:", loadedFiles, "/", imageFiles);
                      
                      if (loadedFiles === imageFiles) {
                          finishLoading();
                      }
                  };
                  
                  img.src = e.target.result;
              };
              
              // 以DataURL格式读取文件
              reader.readAsDataURL(file);
          });
          
          function finishLoading() {
              console.log("所有图像加载完成");
              setTimeout(() => {
                  progressBar.classList.add('hidden');
                  thumbnailsContainer.classList.remove('hidden');
                  infoText.textContent = '点击缩略图选择要编辑的照片';
                  
                  if (window.images.length > 0) {
                      console.log("选择第一张图像");
                      selectImage(0);
                  } else {
                      console.error("没有成功加载图像");
                      alert("未能成功加载任何图像,请重试。");
                  }
              }, 500);
          }
      } catch (error) {
          console.error("处理文件时发生错误:", error);
          alert("上传图片时发生错误: " + error.message);
          progressBar.classList.add('hidden');
      }
    }
    
    // 创建缩略图
    function createThumbnail(img, index) {
      const thumbnail = document.createElement('canvas');
      thumbnail.width = 100;
      thumbnail.height = 100;
      thumbnail.className = 'thumbnail';
      thumbnail.dataset.index = index;
      
      const ctx = thumbnail.getContext('2d');
      const scale = Math.min(100 / img.width, 100 / img.height);
      const width = img.width * scale;
      const height = img.height * scale;
      const x = (100 - width) / 2;
      const y = (100 - height) / 2;
      
      ctx.fillStyle = '#f0f0f0';
      ctx.fillRect(0, 0, 100, 100);
      ctx.drawImage(img, x, y, width, height);
      
      thumbnail.addEventListener('click', () => {
        selectImage(index);
      });
      
      thumbnailsContainer.appendChild(thumbnail);
    }
    
    // =====================================================================
    // 裁剪功能
    // =====================================================================
    
    // 裁剪事件处理函数
    function cropMouseDownHandler(e) {
      if (!window.isCropMode) return;
      
      isDragging = true;
      startX = e.clientX;
      startY = e.clientY;
      initialCropX = cropX;
      initialCropY = cropY;
      
      e.preventDefault();
      e.stopPropagation(); // 阻止事件冒泡
    }
    
    function cropMouseMoveHandler(e) {
        if (!isDragging || !window.isCropMode) return;
        
        // Get the current image and crop container
        const img = window.images[window.currentImageIndex].img;
        const containerWidth = cropContainer.clientWidth;
        
        // Calculate the scale between displayed image and original image
        const scale = img.width / containerWidth;
        
        // Calculate mouse movement distance
        const dx = e.clientX - startX;
        const dy = e.clientY - startY;
        
        // Apply inverse scaling to convert screen pixels to image pixels
        // This makes the drag feel more responsive and accurate
        cropX = Math.max(0, Math.min(img.width - 600, initialCropX + dx * scale));
        cropY = Math.max(0, Math.min(img.height - 600, initialCropY + dy * scale));
        
        updateCropArea();
      }
    
    function cropMouseUpHandler() {
      isDragging = false;
    }

    // 新功能:将当前裁剪应用到所有照片
    function applyCropToAll() {
        if (window.currentImageIndex < 0) return;
    
    // 确保当前裁剪设置有效
        const currentSettings = window.images[window.currentImageIndex].settings;
        if (!currentSettings.isCropped || 
            currentSettings.cropX === null || 
            currentSettings.cropY === null) {
        alert('请先对当前照片进行裁剪');
            return;
        }
    
        const confirmApply = confirm('确定要将当前裁剪设置应用到所有照片吗?\n注意:这可能会导致部分照片裁剪区域超出范围');
        if (!confirmApply) return;
    
        // 获取当前裁剪设置
        const cropX = currentSettings.cropX;
        const cropY = currentSettings.cropY;
        let successCount = 0;
        let failCount = 0;
    
        // 应用到所有照片
        window.images.forEach((image, index) => {
          if (index === window.currentImageIndex) return; // 跳过当前照片
      
          const img = image.img;
      
          // 检查裁剪区域是否在图像范围内
          if (cropX >= 0 && cropY >= 0 && 
              cropX + 600 <= img.width && 
              cropY + 600 <= img.height) {
            // 应用裁剪设置
            image.settings.cropX = cropX;
            image.settings.cropY = cropY;
            image.settings.isCropped = true;
            image.isEdited = true;
            successCount++;
          } else {
            failCount++;
          }
        });
        
        // 显示结果
        if (failCount > 0) {
          alert(`裁剪已应用到 ${successCount} 张照片,${failCount} 张照片因尺寸不足无法应用相同裁剪`);
        } else {
          alert(`裁剪已成功应用到所有 ${successCount} 张照片`);
    }
    
    // 更新当前显示
    if (typeof renderImage === 'function') {
      renderImage();
    }
  }
  
    // 更新裁剪区域显示
    function updateCropArea() {
      const img = window.images[window.currentImageIndex].img;
      
      // 计算缩放比例
      const containerWidth = cropContainer.clientWidth;
      const scale = containerWidth / img.width;
      
      // 调整裁剪区域定位
      cropArea.style.width = (600 * scale) + 'px';
      cropArea.style.height = (600 * scale) + 'px';
      cropArea.style.left = (cropX * scale) + 'px';
      cropArea.style.top = (cropY * scale) + 'px';
    }
  
    // 退出裁剪模式
    function exitCropMode() {
      console.log("退出裁剪模式");
      
      if (!window.isCropMode) return;
      
      // 移除事件监听器
      cropImage.removeEventListener('mousedown', cropMouseDownHandler);
      document.removeEventListener('mousemove', cropMouseMoveHandler);
      document.removeEventListener('mouseup', cropMouseUpHandler);
      
      originalCanvas.classList.remove('hidden');
      cropContainer.classList.add('hidden');
      enableCropButton.classList.remove('hidden');
      applyCropButton.classList.add('hidden');
      cancelCropButton.classList.add('hidden');
      resetCropButton.classList.add('hidden');
      
      // 重新启用控件
      tempSlider.disabled = false;
      tintSlider.disabled = false;
      brightnessSlider.disabled = false;
      contrastSlider.disabled = false;
      saturationSlider.disabled = false;
      whitePicker.disabled = false;
      
      window.isCropMode = false;
    }
    
    // 进入裁剪模式
    function enterCropMode() {
      console.log("进入裁剪模式");
      
      try {
        // 获取当前选中的图像索引
        if (window.currentImageIndex < 0) {
          alert("请先选择一张图片");
          return;
        }
        
        // 隐藏原始画布,显示裁剪容器
        originalCanvas.classList.add('hidden');
        cropContainer.classList.remove('hidden');
        
        // 隐藏启用按钮,显示裁剪操作按钮
        enableCropButton.classList.add('hidden');
        applyCropButton.classList.remove('hidden');
        cancelCropButton.classList.remove('hidden');
        resetCropButton.classList.remove('hidden');
        
        // 设置裁剪图像
        const selectedImage = window.images[window.currentImageIndex].img;
        cropImage.src = selectedImage.src;
        
        // 设置初始裁剪区域位置 (居中)
        cropX = Math.max(0, (selectedImage.width - 600) / 2);
        cropY = Math.max(0, (selectedImage.height - 600) / 2);
        
        // 更新裁剪区域UI
        updateCropArea();
        
        // 标记为已进入裁剪模式
        window.isCropMode = true;
        
        // 禁用其他控件,防止冲突
        tempSlider.disabled = true;
        tintSlider.disabled = true;
        brightnessSlider.disabled = true;
        contrastSlider.disabled = true;
        saturationSlider.disabled = true;
        whitePicker.disabled = true;
        
        // 为cropImage添加拖动事件
        setupCropDragEvents();
      } catch (err) {
        console.error("启用裁剪时出错:", err);
        alert("启用裁剪时出错: " + err.message);
      }
    }
    
    // 设置裁剪拖动事件
    function setupCropDragEvents() {
      // 先移除可能存在的旧事件监听器
      cropImage.removeEventListener('mousedown', cropMouseDownHandler);
      document.removeEventListener('mousemove', cropMouseMoveHandler);
      document.removeEventListener('mouseup', cropMouseUpHandler);
      
      // 使用命名函数而不是匿名函数,便于移除
      cropImage.addEventListener('mousedown', cropMouseDownHandler);
      document.addEventListener('mousemove', cropMouseMoveHandler);
      document.addEventListener('mouseup', cropMouseUpHandler);
    }
    
    // 应用裁剪
    function applyCrop() {
      console.log("应用裁剪");
      
      try {
        if (!window.isCropMode || window.currentImageIndex < 0) return;
        
        // 保存裁剪设置到当前图像
        window.images[window.currentImageIndex].settings.cropX = cropX;
        window.images[window.currentImageIndex].settings.cropY = cropY;
        window.images[window.currentImageIndex].settings.isCropped = true;
        
        // 标记为已编辑
        markAsEdited();
        
        // 退出裁剪模式
        exitCropMode();
        
        // 重新渲染图像
        renderImage();
        
        console.log("裁剪已应用:", {
          cropX, cropY, 
          imgWidth: window.images[window.currentImageIndex].img.width,
          imgHeight: window.images[window.currentImageIndex].img.height
        });
      } catch (err) {
        console.error("应用裁剪时出错:", err);
        alert("应用裁剪时出错: " + err.message);
      }
    }
    
    // =====================================================================
    // 图像处理核心功能
    // =====================================================================
    
    // 选择图像
    function selectImage(index) {
      if (index < 0 || index >= window.images.length) return;
      
      // 如果之前在裁剪模式,先退出
      exitCropMode();
      
      window.currentImageIndex = index;
      currentSettings = { ...window.images[index].settings };
      
      // 更新缩略图选择状态
      const thumbnails = thumbnailsContainer.querySelectorAll('.thumbnail');
      thumbnails.forEach(thumb => {
        thumb.classList.remove('selected');
        if (parseInt(thumb.dataset.index) === index) {
          thumb.classList.add('selected');
        }
      });
      
      // 更新编辑器
      editor.classList.remove('hidden');
      updateSliders();
      
      // 确保renderImage已定义
      if (typeof renderImage === 'function') {
        renderImage();
      } else {
        console.error("renderImage函数未定义,无法渲染图像");
      }
      
      // 重置白点选择器状态
      isWhitePickerActive = false;
      whitePicker.textContent = '选择白点';
      originalCanvas.classList.remove('eyedropper');
    }
    
    // 更新滑块
    function updateSliders() {
      tempSlider.value = currentSettings.temp;
      tintSlider.value = currentSettings.tint;
      brightnessSlider.value = currentSettings.brightness;
      contrastSlider.value = currentSettings.contrast;
      saturationSlider.value = currentSettings.saturation;
      
      tempValue.textContent = currentSettings.temp + 'K';
      tintValue.textContent = currentSettings.tint > 0 
        ? '+' + currentSettings.tint + ' (偏绿)' 
        : currentSettings.tint < 0 
          ? currentSettings.tint + ' (偏紫)' 
          : '0';
      brightnessValue.textContent = currentSettings.brightness;
      contrastValue.textContent = currentSettings.contrast;
      saturationValue.textContent = currentSettings.saturation;
    }
    
    // 保存当前设置
    function saveCurrentSettings() {
      if (window.currentImageIndex >= 0) {
        window.images[window.currentImageIndex].settings = { ...currentSettings };
      }
    }
    
    // 标记图像为已编辑
    function markAsEdited() {
      if (window.currentImageIndex >= 0) {
        window.images[window.currentImageIndex].isEdited = true;
      }
    }
    
    // 暴露全局函数供内联脚本调用
    window.enterCropMode = enterCropMode;  
    window.enableCropMode = enterCropMode; 
    window.applyCropMode = applyCrop;
    window.cancelCropMode = exitCropMode;
    window.handleFiles = handleFiles;
    window.saveCurrentSettings = saveCurrentSettings;
    window.markAsEdited = markAsEdited;
    window.updateSliders = updateSliders;
    window.renderImage = renderImage;
    window.selectImage = selectImage;
    window.updateCropArea = updateCropArea;
    window.exitCropMode = exitCropMode;
    window.calculateWhiteBalance = calculateWhiteBalance;
    
    console.log("函数已暴露到全局:", 
      "enterCropMode=", typeof window.enterCropMode,
      "enableCropMode=", typeof window.enableCropMode,
      "applyCropMode=", typeof window.applyCropMode,
      "cancelCropMode=", typeof window.cancelCropMode);
    
    // =====================================================================
    // 事件监听器
    // =====================================================================
    
    // 重置裁剪
    resetCropButton.addEventListener('click', function() {
      if (!window.isCropMode || window.currentImageIndex < 0) return;
      
      // 如果之前已应用裁剪,则取消裁剪
      window.images[window.currentImageIndex].settings.isCropped = false;
      window.images[window.currentImageIndex].settings.cropX = null;
      window.images[window.currentImageIndex].settings.cropY = null;
      
      // 标记为已编辑
      markAsEdited();
      
      // 退出裁剪模式
      exitCropMode();
      
      // 重新渲染图像
      if (typeof renderImage === 'function') {
        renderImage();
      }
      
      console.log("裁剪已重置");
    });
    
    // 连接裁剪按钮事件
    enableCropButton.addEventListener('click', function() {
      enterCropMode();
    });
    
    applyCropButton.addEventListener('click', function() {
      applyCrop();
    });
    
    cancelCropButton.addEventListener('click', function() {
      exitCropMode();
    });

    applyCropAllButton.addEventListener('click', function() {
        applyCropToAll();
      });
    
    // 滑块事件监听
    tempSlider.addEventListener('input', () => {
        currentSettings.temp = parseInt(tempSlider.value);
        tempValue.textContent = currentSettings.temp + 'K';
        
        // 实时更新图像设置,以便renderImage能够看到最新的设置
        if (window.currentImageIndex >= 0) {
          window.images[window.currentImageIndex].settings.temp = currentSettings.temp;
        }
        
        if (typeof renderImage === 'function') {
          renderImage();
        }
      });
    
    tintSlider.addEventListener('input', () => {
        currentSettings.tint = parseInt(tintSlider.value);
        tintValue.textContent = currentSettings.tint > 0 
          ? '+' + currentSettings.tint + ' (偏绿)' 
          : currentSettings.tint < 0 
            ? currentSettings.tint + ' (偏紫)' 
            : '0';
        
        // 实时更新图像设置
        if (window.currentImageIndex >= 0) {
          window.images[window.currentImageIndex].settings.tint = currentSettings.tint;
        }
        
        if (typeof renderImage === 'function') {
          renderImage();
        }
    });
    
    brightnessSlider.addEventListener('input', () => {
        currentSettings.brightness = parseInt(brightnessSlider.value);
        brightnessValue.textContent = currentSettings.brightness;
        
        // 实时更新图像设置
        if (window.currentImageIndex >= 0) {
            window.images[window.currentImageIndex].settings.brightness = currentSettings.brightness;
          }
          
          if (typeof renderImage === 'function') {
            renderImage();
          }
    });
    
    contrastSlider.addEventListener('input', () => {
        currentSettings.contrast = parseInt(contrastSlider.value);
        contrastValue.textContent = currentSettings.contrast;
        
        // 实时更新图像设置
        if (window.currentImageIndex >= 0) {
          window.images[window.currentImageIndex].settings.contrast = currentSettings.contrast;
        }
        
        if (typeof renderImage === 'function') {
          renderImage();
        }
      });
    
    saturationSlider.addEventListener('input', () => {
        currentSettings.saturation = parseInt(saturationSlider.value);
        saturationValue.textContent = currentSettings.saturation;
        
        // 实时更新图像设置
        if (window.currentImageIndex >= 0) {
          window.images[window.currentImageIndex].settings.saturation = currentSettings.saturation;
        }
        
        if (typeof renderImage === 'function') {
          renderImage();
        }
    });
    
    // 滑块事件监听 - 保存设置
    tempSlider.addEventListener('change', () => {
        // 保存当前设置
        saveCurrentSettings();
        // 标记图像为已编辑
        markAsEdited();
      });
      
    tintSlider.addEventListener('change', () => {
        saveCurrentSettings();
        markAsEdited();
    });
      
    brightnessSlider.addEventListener('change', () => {
        saveCurrentSettings();
        markAsEdited();
    });
      
    contrastSlider.addEventListener('change', () => {
        saveCurrentSettings();
        markAsEdited();
    });
      
    saturationSlider.addEventListener('change', () => {
        saveCurrentSettings();
        markAsEdited();
    });

    
    // 重置按钮
    resetButton.addEventListener('click', () => {
      // 如果在裁剪模式中,先退出
      if (window.isCropMode) {
        exitCropMode();
      }
      
      // 重置白平衡和曝光设置,但保留裁剪设置
      const { cropX, cropY, isCropped } = currentSettings;
      currentSettings = { 
        ...defaultSettings,
        cropX,
        cropY,
        isCropped
      };
      
      updateSliders();
      if (typeof renderImage === 'function') {
        renderImage();
      }
      saveCurrentSettings();
    });
    
    // 白平衡取样
    whitePicker.addEventListener('click', () => {
      console.log("白点取样按钮被点击");
      if (isWhitePickerActive) {
        isWhitePickerActive = false;
        whitePicker.textContent = '选择白点';
        originalCanvas.classList.remove('eyedropper');
      } else {
        isWhitePickerActive = true;
        whitePicker.textContent = '取消选择';
        originalCanvas.classList.add('eyedropper');
      }
    });
    
    // 白点取样事件
    originalCanvas.addEventListener('click', e => {
      if (!isWhitePickerActive) return;
      
      const rect = originalCanvas.getBoundingClientRect();
      const x = Math.floor((e.clientX - rect.left) / rect.width * originalCanvas.width);
      const y = Math.floor((e.clientY - rect.top) / rect.height * originalCanvas.height);
      
      originalCtx = originalCanvas.getContext('2d', { willReadFrequently: true });
      if (!originalCtx) {
        console.error("无法获取画布上下文");
        return;
      }
      
      const pixel = originalCtx.getImageData(x, y, 1, 1).data;
      const r = pixel[0];
      const g = pixel[1];
      const b = pixel[2];
      
      whitePreview.style.backgroundColor = `rgb(${r},${g},${b})`;
      
      // 根据选择的白点计算颜色校正
      if (typeof calculateWhiteBalance === 'function') {
        calculateWhiteBalance(r, g, b);
      } else {
        console.error("calculateWhiteBalance函数未定义");
      }
      
      // 退出取样模式
      isWhitePickerActive = false;
      whitePicker.textContent = '选择白点';
      originalCanvas.classList.remove('eyedropper');
    });
  });

image-editor.js

// 主程序
document.addEventListener('DOMContentLoaded', function() {
    // 变量声明
    const uploadSection = document.getElementById('upload-section');
    const fileInput = document.getElementById('file-input');
    const progressBar = document.getElementById('progress-bar');
    const progress = document.getElementById('progress');
    const thumbnailsContainer = document.getElementById('thumbnails');
    const editor = document.getElementById('editor');
    const infoText = document.getElementById('info-text');
    const originalCanvas = document.getElementById('original-canvas');
    const editedCanvas = document.getElementById('edited-canvas');
    const tempSlider = document.getElementById('temp-slider');
    const tintSlider = document.getElementById('tint-slider');
    const brightnessSlider = document.getElementById('brightness-slider');
    const contrastSlider = document.getElementById('contrast-slider');
    const saturationSlider = document.getElementById('saturation-slider');
    const tempValue = document.getElementById('temp-value');
    const tintValue = document.getElementById('tint-value');
    const brightnessValue = document.getElementById('brightness-value');
    const contrastValue = document.getElementById('contrast-value');
    const saturationValue = document.getElementById('saturation-value');
    const resetButton = document.getElementById('reset-button');
    const downloadButton = document.getElementById('download-button');
    const downloadAllButton = document.getElementById('download-all-button');
    const applyAllButton = document.getElementById('apply-all');
    const whitePicker = document.getElementById('white-picker');
    const whitePreview = document.getElementById('white-preview');
    
    // 裁剪相关元素
    const enableCropButton = document.getElementById('enable-crop');
    const applyCropButton = document.getElementById('apply-crop');
    const cancelCropButton = document.getElementById('cancel-crop');
    const resetCropButton = document.getElementById('reset-crop');
    const applyCropAllButton = document.getElementById('apply-crop-all');
    const cropContainer = document.getElementById('crop-container');
    const cropImage = document.getElementById('crop-image');
    const cropArea = document.getElementById('crop-area');
  
    // 存储图像数据和编辑设置 - 使用全局变量以便内联脚本访问
    window.images = [];
    window.currentImageIndex = -1;
    let originalCtx, editedCtx;
    let isWhitePickerActive = false;
    window.isCropMode = false;
    let isDragging = false;
    let startX = 0;
    let startY = 0;
    let initialCropX = 0;
    let initialCropY = 0;
    let cropX = 0;
    let cropY = 0;
    
    // 默认设置
    const defaultSettings = {
      temp: 5500,  // 默认色温值 (K)
      tint: 0,     // 色调(紫-绿)
      brightness: 0,
      contrast: 0,
      saturation: 0,
      cropX: null, // 裁剪X起点
      cropY: null, // 裁剪Y起点
      isCropped: false // 是否已裁剪
    };
    
    // 当前设置
    let currentSettings = { ...defaultSettings };
  
    console.log("初始化上传功能...");
    console.log("上传区域元素:", uploadSection ? "已找到" : "未找到");
    console.log("文件输入元素:", fileInput ? "已找到" : "未找到");
    
    // 从imageEditor获取函数,确保在使用前已定义
    let renderImage, applyAdjustments, calculateWhiteBalance;
    
    if (window.imageEditor) {
      renderImage = window.imageEditor.renderImage;
      applyAdjustments = window.imageEditor.applyAdjustments;
      calculateWhiteBalance = window.imageEditor.calculateWhiteBalance;
      
      // 设置下载按钮事件
      if (downloadButton) {
        downloadButton.addEventListener('click', window.imageEditor.downloadImage);
      }
      
      if (downloadAllButton) {
        downloadAllButton.addEventListener('click', window.imageEditor.downloadAllImages);
      }
      
      if (applyAllButton) {
        applyAllButton.addEventListener('click', window.imageEditor.applySettingsToAll);
      }
    } else {
      console.error("imageEditor模块未找到,核心功能将不可用");
      // 定义临时空函数,避免报错
      renderImage = function() {
        console.error("renderImage函数未定义");
      };
      applyAdjustments = function() {
        console.error("applyAdjustments函数未定义");
      };
      calculateWhiteBalance = function() {
        console.error("calculateWhiteBalance函数未定义");
      };
    }
    
    // =====================================================================
    // 文件上传和处理功能
    // =====================================================================
    
    // 设置上传区域事件 - 确保事件正确绑定
    if (uploadSection) {
        uploadSection.onclick = function(e) {
            console.log("上传区域被点击");
            if (fileInput) {
                fileInput.click();
            } else {
                console.error("找不到文件输入元素!");
            }
        };
        
        uploadSection.ondragover = function(e) {
            e.preventDefault();
            e.stopPropagation();
            uploadSection.style.borderColor = '#2e7d32';
            console.log("文件拖动到上传区域上方");
        };
        
        uploadSection.ondragleave = function(e) {
            e.preventDefault();
            e.stopPropagation();
            uploadSection.style.borderColor = '#ccc';
            console.log("文件离开上传区域");
        };
        
        uploadSection.ondrop = function(e) {
            console.log("文件被拖放到上传区域");
            e.preventDefault();
            e.stopPropagation();
            uploadSection.style.borderColor = '#ccc';
            
            if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
                console.log("检测到", e.dataTransfer.files.length, "个文件");
                handleFiles(e.dataTransfer.files);
            } else {
                console.error("未检测到文件或文件列表为空");
            }
        };
    } else {
        console.error("找不到上传区域元素!");
    }
    
    // 文件输入事件
    if (fileInput) {
      fileInput.onchange = function(e) {
          console.log("文件输入发生变化");
          if (this.files && this.files.length > 0) {
              console.log("选择了", this.files.length, "个文件");
              handleFiles(this.files);
          } else {
              console.error("未选择文件或文件列表为空");
          }
      };
    } else {
      console.error("找不到文件输入元素!");
    }
    
    // 处理上传的文件
    function handleFiles(files) {
      try {
          console.log("开始处理文件...");
          progressBar.classList.remove('hidden');
          window.images = [];
          thumbnailsContainer.innerHTML = '';
          window.currentImageIndex = -1;
          
          const totalFiles = files.length;
          let loadedFiles = 0;
          let imageFiles = 0;
          
          console.log("总文件数:", totalFiles);
          
          // 先检查有多少个图像文件
          for (let i = 0; i < files.length; i++) {
              if (files[i].type.startsWith('image/')) {
                  imageFiles++;
              } else {
                  console.warn("文件不是图像:", files[i].name, files[i].type);
              }
          }
          
          if (imageFiles === 0) {
              console.error("没有可处理的图像文件");
              progressBar.classList.add('hidden');
              alert("请选择图像文件 (JPG, PNG, GIF等)");
              return;
          }
          
          console.log("有效图像文件数:", imageFiles);
          
          // 处理每个文件
          Array.from(files).forEach((file, index) => {
              if (!file.type.startsWith('image/')) {
                  console.log("跳过非图像文件:", file.name);
                  return;
              }
              
              console.log("处理图像文件:", file.name, "类型:", file.type);
              
              const reader = new FileReader();
              
              reader.onerror = function(error) {
                  console.error("读取文件时出错:", error);
                  loadedFiles++;
                  progress.style.width = (loadedFiles / imageFiles * 100) + '%';
                  
                  if (loadedFiles === imageFiles) {
                      finishLoading();
                  }
              };
              
              reader.onload = function(e) {
                  console.log("文件读取完成:", file.name);
                  
                  const img = new Image();
                  
                  img.onerror = function() {
                      console.error("图像加载失败:", file.name);
                      loadedFiles++;
                      progress.style.width = (loadedFiles / imageFiles * 100) + '%';
                      
                      if (loadedFiles === imageFiles) {
                          finishLoading();
                      }
                  };
                  
                  img.onload = function() {
                      console.log("图像加载成功:", file.name, img.width, "x", img.height);
                      
                      window.images.push({
                          img: img,
                          name: file.name,
                          settings: { ...defaultSettings },
                          isEdited: false // 标记是否已编辑
                      });
                      
                      createThumbnail(img, window.images.length - 1);
                      
                      loadedFiles++;
                      progress.style.width = (loadedFiles / imageFiles * 100) + '%';
                      
                      console.log("进度:", loadedFiles, "/", imageFiles);
                      
                      if (loadedFiles === imageFiles) {
                          finishLoading();
                      }
                  };
                  
                  img.src = e.target.result;
              };
              
              // 以DataURL格式读取文件
              reader.readAsDataURL(file);
          });
          
          function finishLoading() {
              console.log("所有图像加载完成");
              setTimeout(() => {
                  progressBar.classList.add('hidden');
                  thumbnailsContainer.classList.remove('hidden');
                  infoText.textContent = '点击缩略图选择要编辑的照片';
                  
                  if (window.images.length > 0) {
                      console.log("选择第一张图像");
                      selectImage(0);
                  } else {
                      console.error("没有成功加载图像");
                      alert("未能成功加载任何图像,请重试。");
                  }
              }, 500);
          }
      } catch (error) {
          console.error("处理文件时发生错误:", error);
          alert("上传图片时发生错误: " + error.message);
          progressBar.classList.add('hidden');
      }
    }
    
    // 创建缩略图
    function createThumbnail(img, index) {
      const thumbnail = document.createElement('canvas');
      thumbnail.width = 100;
      thumbnail.height = 100;
      thumbnail.className = 'thumbnail';
      thumbnail.dataset.index = index;
      
      const ctx = thumbnail.getContext('2d');
      const scale = Math.min(100 / img.width, 100 / img.height);
      const width = img.width * scale;
      const height = img.height * scale;
      const x = (100 - width) / 2;
      const y = (100 - height) / 2;
      
      ctx.fillStyle = '#f0f0f0';
      ctx.fillRect(0, 0, 100, 100);
      ctx.drawImage(img, x, y, width, height);
      
      thumbnail.addEventListener('click', () => {
        selectImage(index);
      });
      
      thumbnailsContainer.appendChild(thumbnail);
    }
    
    // =====================================================================
    // 裁剪功能
    // =====================================================================
    
    // 裁剪事件处理函数
    function cropMouseDownHandler(e) {
      if (!window.isCropMode) return;
      
      isDragging = true;
      startX = e.clientX;
      startY = e.clientY;
      initialCropX = cropX;
      initialCropY = cropY;
      
      e.preventDefault();
      e.stopPropagation(); // 阻止事件冒泡
    }
    
    function cropMouseMoveHandler(e) {
        if (!isDragging || !window.isCropMode) return;
        
        // Get the current image and crop container
        const img = window.images[window.currentImageIndex].img;
        const containerWidth = cropContainer.clientWidth;
        
        // Calculate the scale between displayed image and original image
        const scale = img.width / containerWidth;
        
        // Calculate mouse movement distance
        const dx = e.clientX - startX;
        const dy = e.clientY - startY;
        
        // Apply inverse scaling to convert screen pixels to image pixels
        // This makes the drag feel more responsive and accurate
        cropX = Math.max(0, Math.min(img.width - 600, initialCropX + dx * scale));
        cropY = Math.max(0, Math.min(img.height - 600, initialCropY + dy * scale));
        
        updateCropArea();
      }
    
    function cropMouseUpHandler() {
      isDragging = false;
    }

    // 新功能:将当前裁剪应用到所有照片
    function applyCropToAll() {
        if (window.currentImageIndex < 0) return;
    
    // 确保当前裁剪设置有效
        const currentSettings = window.images[window.currentImageIndex].settings;
        if (!currentSettings.isCropped || 
            currentSettings.cropX === null || 
            currentSettings.cropY === null) {
        alert('请先对当前照片进行裁剪');
            return;
        }
    
        const confirmApply = confirm('确定要将当前裁剪设置应用到所有照片吗?\n注意:这可能会导致部分照片裁剪区域超出范围');
        if (!confirmApply) return;
    
        // 获取当前裁剪设置
        const cropX = currentSettings.cropX;
        const cropY = currentSettings.cropY;
        let successCount = 0;
        let failCount = 0;
    
        // 应用到所有照片
        window.images.forEach((image, index) => {
          if (index === window.currentImageIndex) return; // 跳过当前照片
      
          const img = image.img;
      
          // 检查裁剪区域是否在图像范围内
          if (cropX >= 0 && cropY >= 0 && 
              cropX + 600 <= img.width && 
              cropY + 600 <= img.height) {
            // 应用裁剪设置
            image.settings.cropX = cropX;
            image.settings.cropY = cropY;
            image.settings.isCropped = true;
            image.isEdited = true;
            successCount++;
          } else {
            failCount++;
          }
        });
        
        // 显示结果
        if (failCount > 0) {
          alert(`裁剪已应用到 ${successCount} 张照片,${failCount} 张照片因尺寸不足无法应用相同裁剪`);
        } else {
          alert(`裁剪已成功应用到所有 ${successCount} 张照片`);
    }
    
    // 更新当前显示
    if (typeof renderImage === 'function') {
      renderImage();
    }
  }
  
    // 更新裁剪区域显示
    function updateCropArea() {
      const img = window.images[window.currentImageIndex].img;
      
      // 计算缩放比例
      const containerWidth = cropContainer.clientWidth;
      const scale = containerWidth / img.width;
      
      // 调整裁剪区域定位
      cropArea.style.width = (600 * scale) + 'px';
      cropArea.style.height = (600 * scale) + 'px';
      cropArea.style.left = (cropX * scale) + 'px';
      cropArea.style.top = (cropY * scale) + 'px';
    }
  
    // 退出裁剪模式
    function exitCropMode() {
      console.log("退出裁剪模式");
      
      if (!window.isCropMode) return;
      
      // 移除事件监听器
      cropImage.removeEventListener('mousedown', cropMouseDownHandler);
      document.removeEventListener('mousemove', cropMouseMoveHandler);
      document.removeEventListener('mouseup', cropMouseUpHandler);
      
      originalCanvas.classList.remove('hidden');
      cropContainer.classList.add('hidden');
      enableCropButton.classList.remove('hidden');
      applyCropButton.classList.add('hidden');
      cancelCropButton.classList.add('hidden');
      resetCropButton.classList.add('hidden');
      
      // 重新启用控件
      tempSlider.disabled = false;
      tintSlider.disabled = false;
      brightnessSlider.disabled = false;
      contrastSlider.disabled = false;
      saturationSlider.disabled = false;
      whitePicker.disabled = false;
      
      window.isCropMode = false;
    }
    
    // 进入裁剪模式
    function enterCropMode() {
      console.log("进入裁剪模式");
      
      try {
        // 获取当前选中的图像索引
        if (window.currentImageIndex < 0) {
          alert("请先选择一张图片");
          return;
        }
        
        // 隐藏原始画布,显示裁剪容器
        originalCanvas.classList.add('hidden');
        cropContainer.classList.remove('hidden');
        
        // 隐藏启用按钮,显示裁剪操作按钮
        enableCropButton.classList.add('hidden');
        applyCropButton.classList.remove('hidden');
        cancelCropButton.classList.remove('hidden');
        resetCropButton.classList.remove('hidden');
        
        // 设置裁剪图像
        const selectedImage = window.images[window.currentImageIndex].img;
        cropImage.src = selectedImage.src;
        
        // 设置初始裁剪区域位置 (居中)
        cropX = Math.max(0, (selectedImage.width - 600) / 2);
        cropY = Math.max(0, (selectedImage.height - 600) / 2);
        
        // 更新裁剪区域UI
        updateCropArea();
        
        // 标记为已进入裁剪模式
        window.isCropMode = true;
        
        // 禁用其他控件,防止冲突
        tempSlider.disabled = true;
        tintSlider.disabled = true;
        brightnessSlider.disabled = true;
        contrastSlider.disabled = true;
        saturationSlider.disabled = true;
        whitePicker.disabled = true;
        
        // 为cropImage添加拖动事件
        setupCropDragEvents();
      } catch (err) {
        console.error("启用裁剪时出错:", err);
        alert("启用裁剪时出错: " + err.message);
      }
    }
    
    // 设置裁剪拖动事件
    function setupCropDragEvents() {
      // 先移除可能存在的旧事件监听器
      cropImage.removeEventListener('mousedown', cropMouseDownHandler);
      document.removeEventListener('mousemove', cropMouseMoveHandler);
      document.removeEventListener('mouseup', cropMouseUpHandler);
      
      // 使用命名函数而不是匿名函数,便于移除
      cropImage.addEventListener('mousedown', cropMouseDownHandler);
      document.addEventListener('mousemove', cropMouseMoveHandler);
      document.addEventListener('mouseup', cropMouseUpHandler);
    }
    
    // 应用裁剪
    function applyCrop() {
      console.log("应用裁剪");
      
      try {
        if (!window.isCropMode || window.currentImageIndex < 0) return;
        
        // 保存裁剪设置到当前图像
        window.images[window.currentImageIndex].settings.cropX = cropX;
        window.images[window.currentImageIndex].settings.cropY = cropY;
        window.images[window.currentImageIndex].settings.isCropped = true;
        
        // 标记为已编辑
        markAsEdited();
        
        // 退出裁剪模式
        exitCropMode();
        
        // 重新渲染图像
        renderImage();
        
        console.log("裁剪已应用:", {
          cropX, cropY, 
          imgWidth: window.images[window.currentImageIndex].img.width,
          imgHeight: window.images[window.currentImageIndex].img.height
        });
      } catch (err) {
        console.error("应用裁剪时出错:", err);
        alert("应用裁剪时出错: " + err.message);
      }
    }
    
    // =====================================================================
    // 图像处理核心功能
    // =====================================================================
    
    // 选择图像
    function selectImage(index) {
      if (index < 0 || index >= window.images.length) return;
      
      // 如果之前在裁剪模式,先退出
      exitCropMode();
      
      window.currentImageIndex = index;
      currentSettings = { ...window.images[index].settings };
      
      // 更新缩略图选择状态
      const thumbnails = thumbnailsContainer.querySelectorAll('.thumbnail');
      thumbnails.forEach(thumb => {
        thumb.classList.remove('selected');
        if (parseInt(thumb.dataset.index) === index) {
          thumb.classList.add('selected');
        }
      });
      
      // 更新编辑器
      editor.classList.remove('hidden');
      updateSliders();
      
      // 确保renderImage已定义
      if (typeof renderImage === 'function') {
        renderImage();
      } else {
        console.error("renderImage函数未定义,无法渲染图像");
      }
      
      // 重置白点选择器状态
      isWhitePickerActive = false;
      whitePicker.textContent = '选择白点';
      originalCanvas.classList.remove('eyedropper');
    }
    
    // 更新滑块
    function updateSliders() {
      tempSlider.value = currentSettings.temp;
      tintSlider.value = currentSettings.tint;
      brightnessSlider.value = currentSettings.brightness;
      contrastSlider.value = currentSettings.contrast;
      saturationSlider.value = currentSettings.saturation;
      
      tempValue.textContent = currentSettings.temp + 'K';
      tintValue.textContent = currentSettings.tint > 0 
        ? '+' + currentSettings.tint + ' (偏绿)' 
        : currentSettings.tint < 0 
          ? currentSettings.tint + ' (偏紫)' 
          : '0';
      brightnessValue.textContent = currentSettings.brightness;
      contrastValue.textContent = currentSettings.contrast;
      saturationValue.textContent = currentSettings.saturation;
    }
    
    // 保存当前设置
    function saveCurrentSettings() {
      if (window.currentImageIndex >= 0) {
        window.images[window.currentImageIndex].settings = { ...currentSettings };
      }
    }
    
    // 标记图像为已编辑
    function markAsEdited() {
      if (window.currentImageIndex >= 0) {
        window.images[window.currentImageIndex].isEdited = true;
      }
    }
    
    // 暴露全局函数供内联脚本调用
    window.enterCropMode = enterCropMode;  
    window.enableCropMode = enterCropMode; 
    window.applyCropMode = applyCrop;
    window.cancelCropMode = exitCropMode;
    window.handleFiles = handleFiles;
    window.saveCurrentSettings = saveCurrentSettings;
    window.markAsEdited = markAsEdited;
    window.updateSliders = updateSliders;
    window.renderImage = renderImage;
    window.selectImage = selectImage;
    window.updateCropArea = updateCropArea;
    window.exitCropMode = exitCropMode;
    window.calculateWhiteBalance = calculateWhiteBalance;
    
    console.log("函数已暴露到全局:", 
      "enterCropMode=", typeof window.enterCropMode,
      "enableCropMode=", typeof window.enableCropMode,
      "applyCropMode=", typeof window.applyCropMode,
      "cancelCropMode=", typeof window.cancelCropMode);
    
    // =====================================================================
    // 事件监听器
    // =====================================================================
    
    // 重置裁剪
    resetCropButton.addEventListener('click', function() {
      if (!window.isCropMode || window.currentImageIndex < 0) return;
      
      // 如果之前已应用裁剪,则取消裁剪
      window.images[window.currentImageIndex].settings.isCropped = false;
      window.images[window.currentImageIndex].settings.cropX = null;
      window.images[window.currentImageIndex].settings.cropY = null;
      
      // 标记为已编辑
      markAsEdited();
      
      // 退出裁剪模式
      exitCropMode();
      
      // 重新渲染图像
      if (typeof renderImage === 'function') {
        renderImage();
      }
      
      console.log("裁剪已重置");
    });
    
    // 连接裁剪按钮事件
    enableCropButton.addEventListener('click', function() {
      enterCropMode();
    });
    
    applyCropButton.addEventListener('click', function() {
      applyCrop();
    });
    
    cancelCropButton.addEventListener('click', function() {
      exitCropMode();
    });

    applyCropAllButton.addEventListener('click', function() {
        applyCropToAll();
      });
    
    // 滑块事件监听
    tempSlider.addEventListener('input', () => {
        currentSettings.temp = parseInt(tempSlider.value);
        tempValue.textContent = currentSettings.temp + 'K';
        
        // 实时更新图像设置,以便renderImage能够看到最新的设置
        if (window.currentImageIndex >= 0) {
          window.images[window.currentImageIndex].settings.temp = currentSettings.temp;
        }
        
        if (typeof renderImage === 'function') {
          renderImage();
        }
      });
    
    tintSlider.addEventListener('input', () => {
        currentSettings.tint = parseInt(tintSlider.value);
        tintValue.textContent = currentSettings.tint > 0 
          ? '+' + currentSettings.tint + ' (偏绿)' 
          : currentSettings.tint < 0 
            ? currentSettings.tint + ' (偏紫)' 
            : '0';
        
        // 实时更新图像设置
        if (window.currentImageIndex >= 0) {
          window.images[window.currentImageIndex].settings.tint = currentSettings.tint;
        }
        
        if (typeof renderImage === 'function') {
          renderImage();
        }
    });
    
    brightnessSlider.addEventListener('input', () => {
        currentSettings.brightness = parseInt(brightnessSlider.value);
        brightnessValue.textContent = currentSettings.brightness;
        
        // 实时更新图像设置
        if (window.currentImageIndex >= 0) {
            window.images[window.currentImageIndex].settings.brightness = currentSettings.brightness;
          }
          
          if (typeof renderImage === 'function') {
            renderImage();
          }
    });
    
    contrastSlider.addEventListener('input', () => {
        currentSettings.contrast = parseInt(contrastSlider.value);
        contrastValue.textContent = currentSettings.contrast;
        
        // 实时更新图像设置
        if (window.currentImageIndex >= 0) {
          window.images[window.currentImageIndex].settings.contrast = currentSettings.contrast;
        }
        
        if (typeof renderImage === 'function') {
          renderImage();
        }
      });
    
    saturationSlider.addEventListener('input', () => {
        currentSettings.saturation = parseInt(saturationSlider.value);
        saturationValue.textContent = currentSettings.saturation;
        
        // 实时更新图像设置
        if (window.currentImageIndex >= 0) {
          window.images[window.currentImageIndex].settings.saturation = currentSettings.saturation;
        }
        
        if (typeof renderImage === 'function') {
          renderImage();
        }
    });
    
    // 滑块事件监听 - 保存设置
    tempSlider.addEventListener('change', () => {
        // 保存当前设置
        saveCurrentSettings();
        // 标记图像为已编辑
        markAsEdited();
      });
      
    tintSlider.addEventListener('change', () => {
        saveCurrentSettings();
        markAsEdited();
    });
      
    brightnessSlider.addEventListener('change', () => {
        saveCurrentSettings();
        markAsEdited();
    });
      
    contrastSlider.addEventListener('change', () => {
        saveCurrentSettings();
        markAsEdited();
    });
      
    saturationSlider.addEventListener('change', () => {
        saveCurrentSettings();
        markAsEdited();
    });

    
    // 重置按钮
    resetButton.addEventListener('click', () => {
      // 如果在裁剪模式中,先退出
      if (window.isCropMode) {
        exitCropMode();
      }
      
      // 重置白平衡和曝光设置,但保留裁剪设置
      const { cropX, cropY, isCropped } = currentSettings;
      currentSettings = { 
        ...defaultSettings,
        cropX,
        cropY,
        isCropped
      };
      
      updateSliders();
      if (typeof renderImage === 'function') {
        renderImage();
      }
      saveCurrentSettings();
    });
    
    // 白平衡取样
    whitePicker.addEventListener('click', () => {
      console.log("白点取样按钮被点击");
      if (isWhitePickerActive) {
        isWhitePickerActive = false;
        whitePicker.textContent = '选择白点';
        originalCanvas.classList.remove('eyedropper');
      } else {
        isWhitePickerActive = true;
        whitePicker.textContent = '取消选择';
        originalCanvas.classList.add('eyedropper');
      }
    });
    
    // 白点取样事件
    originalCanvas.addEventListener('click', e => {
      if (!isWhitePickerActive) return;
      
      const rect = originalCanvas.getBoundingClientRect();
      const x = Math.floor((e.clientX - rect.left) / rect.width * originalCanvas.width);
      const y = Math.floor((e.clientY - rect.top) / rect.height * originalCanvas.height);
      
      originalCtx = originalCanvas.getContext('2d', { willReadFrequently: true });
      if (!originalCtx) {
        console.error("无法获取画布上下文");
        return;
      }
      
      const pixel = originalCtx.getImageData(x, y, 1, 1).data;
      const r = pixel[0];
      const g = pixel[1];
      const b = pixel[2];
      
      whitePreview.style.backgroundColor = `rgb(${r},${g},${b})`;
      
      // 根据选择的白点计算颜色校正
      if (typeof calculateWhiteBalance === 'function') {
        calculateWhiteBalance(r, g, b);
      } else {
        console.error("calculateWhiteBalance函数未定义");
      }
      
      // 退出取样模式
      isWhitePickerActive = false;
      whitePicker.textContent = '选择白点';
      originalCanvas.classList.remove('eyedropper');
    });
  });

app.py

from flask import Flask, render_template, send_from_directory
import os

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/static/<path:path>')
def send_static(path):
    return send_from_directory('static', path)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9017)

requirements.txt

flask
gunicorn

Dockerfile

FROM python:3.12-slim

WORKDIR /app

# Install required packages
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application files
COPY . .

# Expose port 9017
EXPOSE 9017

# Set environment variables
ENV FLASK_APP=app.py
ENV FLASK_ENV=production

# Run the application
CMD ["gunicorn", "--bind", "0.0.0.0:9017", "app:app"]

Docker 部署,占用 9017 端口

docker build -t image-picsadjtool .
docker run -d -p 9017:9017 --name container-picsadjtool --restart always image-picsadjtool

使用技巧:

  1. 上传同一批次照片
  2. 取一张照片给视觉功能的 AI 来提供修改意见


实践

1. 上传

使用6张在夕阳下照片

新界面:on 15Mar.2025

2. 问视觉模型

取一张,询问:推荐白平衡调整值 (Prompt)

3. 在工具中调整

4. 批量导出

1) 点击应用到所有照片

2)下载所有调整后的照片

5. 下载确认

后面就是检查有没有手抖跑焦的,然后是切割到指定尺寸,标识。

这项目拖延的, 第一波绿豆芽已经吃完了,第二波在吃...

附件1:纯浏览器运行

目录结构:

/                         
├── index.html    
├── css/               
│   └── styles.css   
└── js/                   
    ├── image-editor.js    
    └── main.js            

index.html

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>芽豆(绿豆)照片白平衡调整工具</title>
  <link rel="stylesheet" href="css/styles.css">
</head>
<body>
  <h1>芽豆(绿豆)照片白平衡调整工具</h1>
  <div class="container">
    <div id="upload-section" class="upload-section" onclick="document.getElementById('file-input').click();">
      <p>点击或拖放照片到此处</p>
      <p><small>支持多张照片同时上传,每张建议不超过10MB</small></p>
      <input type="file" id="file-input" multiple accept="image/*" style="display: none;" onchange="handleFilesFromInput(this.files)">
      <div class="progress-bar hidden" id="progress-bar">
        <div class="progress" id="progress"></div>
      </div>
    </div>
    
    <div id="info-text">上传照片后即可进行白平衡调整</div>
    
    <div id="thumbnails" class="thumbnail-container hidden"></div>
    
    <div id="editor" class="hidden">
      <div class="controls">
        <div class="control-group">
          <h3>白平衡调整</h3>
          
          <div class="picker-container">
            <label>白点取样:</label>
            <button id="white-picker">选择白点</button>
            <div class="color-preview" id="white-preview"></div>
            <div class="help-text">在图像中点击应该是白色的区域</div>
          </div>
          
          <div class="slider-container">
            <label>色温 (K): <span class="value-display" id="temp-value">5500K</span></label>
            <input type="range" id="temp-slider" min="2500" max="10000" value="5500" step="100">
            <div class="color-indicator"></div>
            <div class="temp-scale">
              <span>冷色调 (蓝)</span>
              <span>暖色调 (黄)</span>
            </div>
          </div>
          
          <div class="slider-container">
            <label>色调: <span class="value-display" id="tint-value">0</span></label>
            <input type="range" id="tint-slider" min="-100" max="100" value="0">
            <div class="tint-indicator"></div>
            <div class="tint-scale">
              <span>紫</span>
              <span>绿</span>
            </div>
          </div>
        </div>
        
        <div class="control-group">
          <div class="control-group">
            <h3>裁剪工具</h3>
            <div class="slider-container">
              <button id="enable-crop">启用裁剪 (600×600)</button>
              <button id="apply-crop" class="hidden">应用裁剪</button>
              <button id="cancel-crop" class="hidden">取消裁剪</button>
              <button id="reset-crop" class="hidden">重置裁剪</button>
              <button id="apply-crop-all">应用裁剪到所有照片</button>
              <div class="help-text">裁剪为固定600×600像素区域,拖动图像定位裁剪区域</div>
            </div>
          
          <h3>曝光调整</h3>
          <div class="slider-container">
            <label>亮度: <span class="value-display" id="brightness-value">0</span></label>
            <input type="range" id="brightness-slider" min="-100" max="100" value="0">
          </div>
          
          <div class="slider-container">
            <label>对比度: <span class="value-display" id="contrast-value">0</span></label>
            <input type="range" id="contrast-slider" min="-100" max="100" value="0">
          </div>
          
          <div class="slider-container">
            <label>饱和度: <span class="value-display" id="saturation-value">0</span></label>
            <input type="range" id="saturation-slider" min="-100" max="100" value="0">
          </div>
        </div>
      </div>
      
      <div class="preview-section">
        <div class="preview-container">
          <h3>原图</h3>
          <div id="crop-container" class="crop-container hidden">
            <img id="crop-image" class="crop-image">
            <div class="crop-overlay"></div>
            <div id="crop-area" class="crop-area"></div>
          </div>
          <canvas id="original-canvas"></canvas>
        </div>
        <div class="preview-container">
          <h3>调整后</h3>
          <canvas id="edited-canvas"></canvas>
        </div>
      </div>
      
      <div class="button-group">
        <button id="apply-all">应用到所有照片</button>
        <button id="reset-button">重置调整</button>
        <button id="download-button">下载调整后的照片</button>
        <button id="download-all-button">下载所有调整后的照片</button>
      </div>
    </div>
  </div>

  <script src="js/image-editor.js"></script>
  <script src="js/main.js"></script>
  <script>
    // 直接处理文件输入的函数
    function handleFilesFromInput(files) {
      console.log("从HTML触发文件处理:", files.length, "个文件");
      if (window.handleFiles) {
        window.handleFiles(files);
      } else {
        console.error("找不到handleFiles函数");
        alert("上传功能初始化失败,请刷新页面重试");
      }
    }
  </script>
</body>
</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值