feat: 重构 NodeJS 版本图片处理工具

This commit is contained in:
2026-06-01 22:16:50 +08:00
parent 0922035a4c
commit d277a6118f
4 changed files with 572 additions and 0 deletions
+208
View File
@@ -0,0 +1,208 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { question, normalizeBasename } = require('./utils');
// 转换 PNG 为 JPG
function convertPngToJpg(pngFile, targetDir, succeedDir) {
const filename = path.basename(pngFile);
const baseNoExt = path.basename(pngFile, path.extname(pngFile));
const baseFilename = normalizeBasename(baseNoExt);
const jpgFile = path.join(targetDir, `${baseFilename}.jpg`);
const succeedJpgFile = path.join(succeedDir, `${baseFilename}.jpg`);
// 如果当前目录或 succeed 目录下同名 JPG 文件已存在,则跳过
if (fs.existsSync(jpgFile) || fs.existsSync(succeedJpgFile)) {
console.log(`⏭️ ${jpgFile} 或 ${succeedJpgFile} 已存在,跳过 PNG 转换`);
return null;
}
try {
execSync(`convert "${pngFile}" -quality 100 "${jpgFile}"`, { stdio: 'inherit' });
console.log(`✅ ${filename} 已转换为 JPG: ${jpgFile}`);
return jpgFile;
} catch (error) {
console.error(`❌ PNG 转换失败: ${filename}`);
return null;
}
}
// 查找源文件
function findSourceFile(sourceDir, baseFilename, originalFilename) {
// 先尝试精确匹配
const exactMatch = path.join(sourceDir, originalFilename);
if (fs.existsSync(exactMatch)) {
return exactMatch;
}
// 尝试常见扩展名
const extensions = ['jpg', 'jpeg', 'JPG', 'JPEG', 'heic', 'heif', 'HIF', 'png', 'PNG', 'tif', 'tiff'];
for (const ext of extensions) {
const candidate = path.join(sourceDir, `${baseFilename}.${ext}`);
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
// 恢复 EXIF 信息
function recoverExif(targetFile, sourceFile, succeedDir) {
const filename = path.basename(targetFile);
const ext = path.extname(filename);
const baseNoExt = path.basename(filename, ext);
let succeedFile = path.join(succeedDir, filename);
// 检查 succeed 目录下是否已经存在同名文件,如果存在则添加时间戳
if (fs.existsSync(succeedFile)) {
const timestamp = Date.now();
const newFilename = `${baseNoExt}_${timestamp}${ext}`;
succeedFile = path.join(succeedDir, newFilename);
console.log(`⚠️ ${filename} 在 succeed 目录下已存在,重命名为 ${newFilename}`);
}
try {
// 使用 exiftool 复制 EXIF 信息
execSync(
`exiftool -overwrite_original -all= -UserComment= -tagsFromFile "${sourceFile}" "${targetFile}"`,
{ stdio: 'inherit' }
);
// 移动到 succeed 文件夹
fs.renameSync(targetFile, succeedFile);
console.log(`✅ 匹配成功:从 ${path.basename(sourceFile)} 复制到 ${path.basename(succeedFile)},已移动到 succeed 文件夹`);
return true;
} catch (error) {
console.error(`❌ EXIF 恢复失败: ${filename}`);
return false;
}
}
// 处理大写 JPG 文件(去除 GPS 信息)
function processUppercaseJpg(targetFile) {
try {
execSync(
`exiftool -overwrite_original -UserComment= -gps:all= -artist="奇趣保罗" -CreatorTool= "${targetFile}"`,
{ stdio: 'inherit' }
);
return true;
} catch (error) {
console.error(`❌ 处理失败: ${path.basename(targetFile)}`);
return false;
}
}
// EXIF 恢复主函数
async function runRecoverExifTool(rl) {
console.log('\n🔧 EXIF 信息恢复工具');
console.log('=' .repeat(50));
// 获取源文件夹(原始 EXIF)
const sourceInput = await question(rl, '\n📁 请输入源文件夹路径 (包含原始 EXIF 的图片): ');
const sourceDir = sourceInput.trim();
if (!sourceDir || !fs.existsSync(sourceDir)) {
console.error(`❌ 源文件夹不存在: ${sourceDir}`);
return;
}
// 获取目标文件夹(被修改的)
const targetInput = await question(rl, '📁 请输入目标文件夹路径 (被修改的图片): ');
const targetDir = targetInput.trim();
if (!targetDir || !fs.existsSync(targetDir)) {
console.error(`❌ 目标文件夹不存在: ${targetDir}`);
return;
}
// 创建 succeed 目录
const succeedDir = path.join(targetDir, 'succeed');
if (!fs.existsSync(succeedDir)) {
fs.mkdirSync(succeedDir, { recursive: true });
console.log(`✅ 已创建 succeed 目录: ${succeedDir}`);
}
console.log('\n🚀 开始处理...\n');
let stats = {
pngConverted: 0,
exifRecovered: 0,
exifFailed: 0,
uppercaseProcessed: 0
};
// 第一步:处理 PNG 文件
console.log('📋 步骤 1: 转换 PNG 文件为 JPG\n');
const pngFiles = fs.readdirSync(targetDir).filter(file => {
const ext = path.extname(file).toLowerCase();
const fullPath = path.join(targetDir, file);
return (ext === '.png') && fs.statSync(fullPath).isFile() && !fullPath.includes('/succeed/');
});
for (const pngFile of pngFiles) {
const fullPath = path.join(targetDir, pngFile);
const result = convertPngToJpg(fullPath, targetDir, succeedDir);
if (result) {
stats.pngConverted++;
}
}
// 第二步:恢复 EXIF 信息
console.log('\n📋 步骤 2: 恢复 EXIF 信息\n');
const jpgFiles = fs.readdirSync(targetDir).filter(file => {
const ext = path.extname(file).toLowerCase();
const fullPath = path.join(targetDir, file);
return (ext === '.jpg' || ext === '.jpeg') && fs.statSync(fullPath).isFile() && !fullPath.includes('/succeed/');
});
for (const jpgFile of jpgFiles) {
const fullPath = path.join(targetDir, jpgFile);
const filename = path.basename(jpgFile);
const baseNoExt = path.basename(jpgFile, path.extname(jpgFile));
const baseFilename = normalizeBasename(baseNoExt);
// 查找源文件
const sourceFile = findSourceFile(sourceDir, baseFilename, filename);
if (sourceFile) {
const result = recoverExif(fullPath, sourceFile, succeedDir);
if (result) {
stats.exifRecovered++;
} else {
stats.exifFailed++;
}
} else {
console.log(`⚠️ ${filename} 未找到对应源文件(尝试的基名:${baseFilename}),保留在原位置`);
stats.exifFailed++;
}
}
// 第三步:处理大写 JPG 文件
console.log('\n📋 步骤 3: 处理大写 JPG 文件(去除 GPS 信息)\n');
const uppercaseJpgFiles = fs.readdirSync(targetDir).filter(file => {
const ext = path.extname(file);
const fullPath = path.join(targetDir, file);
return ext === '.JPG' && fs.statSync(fullPath).isFile() && !fullPath.includes('/succeed/');
});
for (const jpgFile of uppercaseJpgFiles) {
const fullPath = path.join(targetDir, jpgFile);
const result = processUppercaseJpg(fullPath);
if (result) {
stats.uppercaseProcessed++;
}
}
// 显示统计
console.log('\n' + '='.repeat(50));
console.log('📊 处理完成!');
console.log(` 🔄 PNG 转换: ${stats.pngConverted} 个`);
console.log(` ✅ EXIF 恢复成功: ${stats.exifRecovered} 个`);
console.log(` ❌ EXIF 恢复失败: ${stats.exifFailed} 个`);
console.log(` 🔧 大写 JPG 处理: ${stats.uppercaseProcessed} 个`);
console.log(` 📁 成功文件目录: ${succeedDir}`);
}
module.exports = { runRecoverExifTool };