feat: 重构 NodeJS 版本图片处理工具
This commit is contained in:
parent
0922035a4c
commit
d277a6118f
|
|
@ -0,0 +1,149 @@
|
||||||
|
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');
|
||||||
|
|
||||||
|
// 处理单个图片
|
||||||
|
function processImage(inputPath, outputPath) {
|
||||||
|
const filename = path.basename(inputPath);
|
||||||
|
const dimensions = getImageDimensions(inputPath);
|
||||||
|
const { type, 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 newSize = getTargetSize(type, dimensions.width, dimensions.height);
|
||||||
|
console.log(` 目标尺寸: ${newSize}`);
|
||||||
|
execSync(
|
||||||
|
`magick "${inputPath}" -gravity center -resize "${newSize}^" -extent "${newSize}" -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)');
|
||||||
|
|
||||||
|
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} 个文件:`);
|
||||||
|
files.forEach((file, index) => {
|
||||||
|
const inputPath = path.join(sourceDir, file);
|
||||||
|
const dimensions = getImageDimensions(inputPath);
|
||||||
|
const { type, reason } = detectImageType(inputPath, dimensions);
|
||||||
|
console.log(` ${index + 1}. ${file} → ${IMAGE_TYPE_LABELS[type]} (${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 skipped = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
const file = files[i];
|
||||||
|
const inputPath = path.join(sourceDir, file);
|
||||||
|
const outputPath = path.join(targetDir, file);
|
||||||
|
|
||||||
|
console.log(`\n[${i + 1}/${files.length}]`);
|
||||||
|
try {
|
||||||
|
const result = processImage(inputPath, outputPath);
|
||||||
|
if (result) {
|
||||||
|
processed++;
|
||||||
|
} else {
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 处理失败: ${file}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(50));
|
||||||
|
console.log('📊 处理完成!');
|
||||||
|
console.log(` ✅ 成功: ${processed} 个`);
|
||||||
|
console.log(` ⏭️ 跳过: ${skipped} 个`);
|
||||||
|
console.log(` ❌ 失败: ${failed} 个`);
|
||||||
|
console.log(` 📁 输出目录: ${targetDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runFormatTool };
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const { createInterface, showMenu } = require('./utils');
|
||||||
|
const { runFormatTool } = require('./format');
|
||||||
|
const { runRecoverExifTool } = require('./recover-exif');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const rl = createInterface();
|
||||||
|
|
||||||
|
console.log('\n🎨 图片工具箱');
|
||||||
|
console.log('=' .repeat(50));
|
||||||
|
console.log('欢迎使用图片处理工具集合!\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
const options = [
|
||||||
|
'📸 图片格式转换 (压缩并调整尺寸)',
|
||||||
|
'🔧 EXIF 信息恢复 (从原始图片恢复元数据)',
|
||||||
|
'❌ 退出'
|
||||||
|
];
|
||||||
|
|
||||||
|
const choice = await showMenu(rl, '\n请选择功能', options);
|
||||||
|
|
||||||
|
if (choice === -1) {
|
||||||
|
console.log('❌ 无效的选择,请重新输入');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (choice === 0) {
|
||||||
|
// 图片格式转换
|
||||||
|
await runFormatTool(rl);
|
||||||
|
} else if (choice === 1) {
|
||||||
|
// EXIF 信息恢复
|
||||||
|
await runRecoverExifTool(rl);
|
||||||
|
} else if (choice === 2) {
|
||||||
|
// 退出
|
||||||
|
console.log('\n👋 再见!');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 询问是否继续
|
||||||
|
const continueChoice = await new Promise((resolve) => {
|
||||||
|
rl.question('\n是否继续使用其他功能? (y/n): ', resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (continueChoice.toLowerCase() !== 'y' && continueChoice.toLowerCase() !== 'yes') {
|
||||||
|
console.log('\n👋 再见!');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ 发生错误:', error.message);
|
||||||
|
} finally {
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行主程序
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('❌ 程序异常:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
const path = require('path');
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const readline = require('readline');
|
||||||
|
|
||||||
|
const IMAGE_TYPES = {
|
||||||
|
IPHONE: 'iphone',
|
||||||
|
SONY: 'sony',
|
||||||
|
SCREENSHOT: 'screenshot',
|
||||||
|
};
|
||||||
|
|
||||||
|
const IMAGE_TYPE_LABELS = {
|
||||||
|
[IMAGE_TYPES.IPHONE]: '📱 iPhone (4:3)',
|
||||||
|
[IMAGE_TYPES.SONY]: '📷 索尼相机 (3:2)',
|
||||||
|
[IMAGE_TYPES.SCREENSHOT]: '🖼️ 截图 → WebP',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建 readline 接口
|
||||||
|
function createInterface() {
|
||||||
|
return readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提问函数
|
||||||
|
function question(rl, prompt) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
rl.question(prompt, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片尺寸
|
||||||
|
function getImageDimensions(filePath) {
|
||||||
|
try {
|
||||||
|
const width = execSync(`identify -format "%w" "${filePath}"`, { encoding: 'utf-8' }).trim();
|
||||||
|
const height = execSync(`identify -format "%h" "${filePath}"`, { encoding: 'utf-8' }).trim();
|
||||||
|
return { width: parseInt(width), height: parseInt(height) };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`获取图片尺寸失败: ${filePath}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取 EXIF 中的 Make / Model
|
||||||
|
function getExifMakeModel(filePath) {
|
||||||
|
try {
|
||||||
|
const output = execSync(`exiftool -Make -Model -s3 "${filePath}"`, { encoding: 'utf-8' }).trim();
|
||||||
|
const [make = '', model = ''] = output.split('\n');
|
||||||
|
return { make: make.trim(), model: model.trim() };
|
||||||
|
} catch {
|
||||||
|
return { make: '', model: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据宽高比推断 iPhone (4:3) 或索尼 (3:2)
|
||||||
|
function detectTypeFromAspectRatio(width, height) {
|
||||||
|
const ratio = Math.max(width, height) / Math.min(width, height);
|
||||||
|
const diff43 = Math.abs(ratio - 4 / 3);
|
||||||
|
const diff32 = Math.abs(ratio - 3 / 2);
|
||||||
|
return diff43 <= diff32 ? IMAGE_TYPES.IPHONE : IMAGE_TYPES.SONY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测图片类型:iPhone / 索尼相机 / 截图
|
||||||
|
function detectImageType(filePath, dimensions) {
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
|
||||||
|
if (ext === '.png') {
|
||||||
|
return { type: IMAGE_TYPES.SCREENSHOT, reason: 'PNG 格式' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { make, model } = getExifMakeModel(filePath);
|
||||||
|
const makeUpper = make.toUpperCase();
|
||||||
|
const modelUpper = model.toUpperCase();
|
||||||
|
|
||||||
|
if (makeUpper === 'APPLE' || modelUpper.includes('IPHONE')) {
|
||||||
|
return { type: IMAGE_TYPES.IPHONE, reason: `EXIF: ${make} ${model}`.trim() };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (makeUpper.includes('SONY') || modelUpper.includes('ILCE') || modelUpper.startsWith('DSC-')) {
|
||||||
|
return { type: IMAGE_TYPES.SONY, reason: `EXIF: ${make} ${model}`.trim() };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.heic', '.heif'].includes(ext)) {
|
||||||
|
return { type: IMAGE_TYPES.IPHONE, reason: 'HEIC/HEIF 格式' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { width, height } = dimensions;
|
||||||
|
const type = detectTypeFromAspectRatio(width, height);
|
||||||
|
return { type, reason: `宽高比推断 (${width}x${height})` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据类型和方向获取目标尺寸,截图为 null(不裁剪)
|
||||||
|
function getTargetSize(type, width, height) {
|
||||||
|
const landscape = width >= height;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case IMAGE_TYPES.IPHONE:
|
||||||
|
return landscape ? '2000x1500' : '1500x2000';
|
||||||
|
case IMAGE_TYPES.SONY:
|
||||||
|
return landscape ? '2250x1500' : '1500x2250';
|
||||||
|
case IMAGE_TYPES.SCREENSHOT:
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
return landscape ? '2250x1500' : '1500x2250';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取输出扩展名
|
||||||
|
function getOutputExtension(type) {
|
||||||
|
return type === IMAGE_TYPES.SCREENSHOT ? '.webp' : '.jpg';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 规范化文件名(去掉 -Modified 或 _Modified 后缀)
|
||||||
|
function normalizeBasename(name) {
|
||||||
|
// 匹配 -Modified 或 _Modified(不区分大小写)
|
||||||
|
const match = name.match(/^(.*?)[-_]?modified$/i);
|
||||||
|
if (match && match[1]) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示菜单并获取选择
|
||||||
|
async function showMenu(rl, title, options) {
|
||||||
|
console.log(`\n${title}`);
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
options.forEach((option, index) => {
|
||||||
|
console.log(`${index + 1}. ${option}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const choice = await question(rl, '\n请选择 (输入数字): ');
|
||||||
|
const index = parseInt(choice) - 1;
|
||||||
|
|
||||||
|
if (index >= 0 && index < options.length) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createInterface,
|
||||||
|
question,
|
||||||
|
getImageDimensions,
|
||||||
|
getExifMakeModel,
|
||||||
|
detectImageType,
|
||||||
|
detectTypeFromAspectRatio,
|
||||||
|
getTargetSize,
|
||||||
|
getOutputExtension,
|
||||||
|
normalizeBasename,
|
||||||
|
showMenu,
|
||||||
|
IMAGE_TYPES,
|
||||||
|
IMAGE_TYPE_LABELS,
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue