feat: 重构 NodeJS 版本图片处理工具
This commit is contained in:
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user