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
使用技巧:
- 上传同一批次照片
- 取一张照片给视觉功能的 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>