Shell-Scripts/photo-toolkit/utils.js

165 lines
4.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const path = require('path');
const { execSync } = require('child_process');
const readline = require('readline');
const IMAGE_TYPES = {
IPHONE: 'iphone',
SONY: 'sony',
XIAOMI: 'xiaomi',
SCREENSHOT: 'screenshot',
};
const IMAGE_TYPE_LABELS = {
[IMAGE_TYPES.IPHONE]: '📱 iPhone (4:3)',
[IMAGE_TYPES.SONY]: '📷 索尼相机 (3:2)',
[IMAGE_TYPES.XIAOMI]: '📱 小米手机 (4:3)',
[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();
// 我的 iPhone
if (makeUpper === 'APPLE' || modelUpper.includes('IPHONE')) {
return { type: IMAGE_TYPES.IPHONE, reason: `EXIF: ${make} ${model}`.trim() };
}
// 老妈小米 11 青春版
if (makeUpper === 'XIAOMI' || modelUpper.includes('M2101K9C')) {
return { type: IMAGE_TYPES.XIAOMI, 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() };
}
// iPhone
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不裁剪
// 索尼返回 ImageMagick 缩放规格(如 2250x按原图宽高比缩放、不裁剪
function getTargetSize(type, width, height) {
const landscape = width >= height;
switch (type) {
case IMAGE_TYPES.IPHONE:
return { size: landscape ? '2000x1500' : '1500x2000', crop: true };
case IMAGE_TYPES.SONY:
return { size: landscape ? '2250x' : 'x2250', crop: false };
case IMAGE_TYPES.SCREENSHOT:
return null;
default:
return { size: landscape ? '2250x1500' : '1500x2250', crop: true };
}
}
// 获取输出扩展名
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,
};