Files
Shell-Scripts/photo-toolkit/format.js
T

184 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const {
question,
getImageDimensions,
detectImageType,
getTargetSize,
getOutputExtension,
IMAGE_TYPES,
IMAGE_TYPE_LABELS,
} = require('./utils');
// 输出可能是 .jpg 或 .webp,先按文件名探测是否已处理,避免读 EXIF
function findExistingOutput(targetDir, inputFilename) {
const base = path.basename(inputFilename, path.extname(inputFilename));
const candidates = ['.jpg', '.webp'].map((ext) => path.join(targetDir, base + ext));
return candidates.find((p) => fs.existsSync(p)) || null;
}
// 处理单个图片(可传入预检测结果,避免重复 identify / exiftool)
function processImage(inputPath, outputPath, predetect) {
const filename = path.basename(inputPath);
const dimensions = predetect?.dimensions ?? getImageDimensions(inputPath);
const { type, reason } = predetect
? { type: predetect.type, reason: predetect.reason }
: detectImageType(inputPath, dimensions);
const outputExt = getOutputExtension(type);
const newFilename = path.basename(outputPath, path.extname(outputPath)) + outputExt;
const finalOutputPath = path.join(path.dirname(outputPath), newFilename);
if (fs.existsSync(finalOutputPath)) {
console.log(`⏭️ 跳过已存在的文件: ${newFilename}`);
return false;
}
console.log(`\n📸 处理: ${filename}`);
console.log(` 类型: ${IMAGE_TYPE_LABELS[type]} (${reason})`);
console.log(` 尺寸: ${dimensions.width}x${dimensions.height}`);
try {
if (type === IMAGE_TYPES.SCREENSHOT) {
execSync(
`magick "${inputPath}" -auto-orient -gravity center -quality 80 "${finalOutputPath}"`,
{ stdio: 'inherit' }
);
} else {
const target = getTargetSize(type, dimensions.width, dimensions.height);
console.log(` 目标尺寸: ${target.size}${target.crop ? '' : ' (保持比例)'}`);
const resizeArgs = target.crop
? `-gravity center -resize "${target.size}^" -extent "${target.size}"`
: `-resize "${target.size}"`;
execSync(
`magick "${inputPath}" -auto-orient ${resizeArgs} -quality 80 "${finalOutputPath}"`,
{ stdio: 'inherit' }
);
}
console.log(` ✅ 图片转换完成 → ${outputExt}`);
} catch (error) {
console.error(` ❌ 图片转换失败`);
throw error;
}
try {
execSync(
`exiftool -overwrite_original -gps:all= -artist="奇趣保罗" -CreatorTool= "${finalOutputPath}"`,
{ stdio: 'inherit' }
);
console.log(` ✅ 元数据处理完成`);
} catch (error) {
console.error(` ❌ 元数据处理失败`);
throw error;
}
return true;
}
// 格式转换主函数
async function runFormatTool(rl) {
console.log('\n🎨 图片格式转换工具');
console.log('='.repeat(50));
console.log('支持类型: iPhone (4:3 JPG) · 索尼相机 (3:2 JPG) · 截图 (WebP) · 小米手机 (4:3 JPG)');
const inputDir = await question(rl, '\n📁 请输入图片所在目录 (留空使用当前目录): ');
const sourceDir = inputDir.trim() || process.cwd();
if (!fs.existsSync(sourceDir)) {
console.error(`❌ 目录不存在: ${sourceDir}`);
return;
}
const outputDir = await question(rl, '📁 请输入输出目录 (留空使用 output): ');
const targetDir = outputDir.trim() || path.join(sourceDir, 'output');
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
console.log(`✅ 已创建输出目录: ${targetDir}`);
}
const extensions = ['.heic', '.heif', '.png', '.jpg', '.jpeg', '.HEIC', '.HEIF', '.PNG', '.JPG', '.JPEG'];
const files = fs.readdirSync(sourceDir).filter(file => {
const ext = path.extname(file);
const fullPath = path.join(sourceDir, file);
const isFile = fs.statSync(fullPath).isFile();
const hasValidExt = extensions.includes(ext);
const notModified = !file.includes('MODIFIED') && !file.includes('MERGED');
return isFile && hasValidExt && notModified;
});
if (files.length === 0) {
console.log('\n⚠️ 未找到符合条件的图片文件');
return;
}
console.log(`\n📋 源目录共 ${files.length} 个文件,先检查 output 中已存在的结果...`);
const pendingFiles = [];
let skipped = 0;
for (const file of files) {
const existing = findExistingOutput(targetDir, file);
if (existing) {
skipped++;
console.log(` ⏭️ ${file} → 已存在 ${path.basename(existing)}`);
} else {
pendingFiles.push(file);
}
}
if (pendingFiles.length === 0) {
console.log(`\n✅ 全部 ${files.length} 个文件已在输出目录中,无需处理`);
return;
}
console.log(`\n🔍 分析剩余 ${pendingFiles.length} 个文件的相机/类型(已跳过 ${skipped} 个)...`);
const jobs = pendingFiles.map((file) => {
const inputPath = path.join(sourceDir, file);
const dimensions = getImageDimensions(inputPath);
const { type, reason } = detectImageType(inputPath, dimensions);
return { file, inputPath, dimensions, type, reason };
});
console.log(`\n📋 待处理 ${jobs.length} 个文件:`);
jobs.forEach((job, index) => {
console.log(` ${index + 1}. ${job.file} → ${IMAGE_TYPE_LABELS[job.type]} (${job.reason})`);
});
const confirm = await question(rl, '\n❓ 是否开始处理? (y/n): ');
if (confirm.toLowerCase() !== 'y' && confirm.toLowerCase() !== 'yes') {
console.log('❌ 已取消');
return;
}
console.log('\n🚀 开始处理...\n');
let processed = 0;
let failed = 0;
for (let i = 0; i < jobs.length; i++) {
const job = jobs[i];
const outputPath = path.join(targetDir, job.file);
console.log(`\n[${i + 1}/${jobs.length}]`);
try {
const result = processImage(job.inputPath, outputPath, job);
if (result) {
processed++;
} else {
skipped++;
}
} catch (error) {
console.error(`❌ 处理失败: ${job.file}`);
failed++;
}
}
console.log('\n' + '='.repeat(50));
console.log('📊 处理完成!');
console.log(` ✅ 成功: ${processed} 个`);
console.log(` ⏭️ 跳过: ${skipped} 个`);
console.log(` ❌ 失败: ${failed} 个`);
console.log(` 📁 输出目录: ${targetDir}`);
}
module.exports = { runFormatTool };