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}" -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}" ${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 };