154 lines
4.2 KiB
JavaScript
154 lines
4.2 KiB
JavaScript
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,
|
||
};
|