/** * fun 结点:img-scale — 图片等比缩放(宽高同乘同一比例) * * 入参(inVars 顺序,与 img-cropping 一致:先路径再参数): * - imagePath:源图(相对流程目录或绝对路径) * - savePath:输出路径 * - scale:缩放系数。支持: * - 0.8 → 长宽各 ×0.8 * - "80%" → 80%;整数 10~100 视为百分比,如 80 即 80% * - 1~10 之间的小数/整数为倍率,如 2 即 2 倍、1.5 即 1.5 倍(整数 10 为 10% 而非 10 倍) */ const path = require('path') const fs = require('fs') const { spawnSync } = require('child_process') const { getPythonExeFromConfig } = require('../../../../python-exe-from-config.js') const tagName = 'img-scale' const configPath = process.env.STATIC_ROOT ? path.join(path.dirname(path.resolve(process.env.STATIC_ROOT)), 'config.js') : path.join(__dirname, '..', '..', '..', '..', '..', 'config.js') const config = fs.existsSync(configPath) ? require(configPath) : {} const projectRoot = (config.projectRoot && fs.existsSync(config.projectRoot)) ? config.projectRoot : path.dirname(path.resolve(configPath)) const scaleScriptPath = path.join(projectRoot, 'python', 'scripts', 'img-scale-proportional.py') function buildAbsolutePath (p, folderPath) { if (p == null || p === '') return null const s = typeof p === 'string' ? p.trim() : String(p) if (!s) return null if (path.isAbsolute(s) || /^[A-Za-z]:/.test(s)) return path.normalize(s) return folderPath ? path.join(folderPath, s) : path.resolve(projectRoot, s) } /** * @param {{ imagePath: string, savePath: string, scale: string|number, folderPath?: string }} input */ async function executeImgScale ({ imagePath, savePath, scale, folderPath }) { if (imagePath == null || String(imagePath).trim() === '') { return { success: false, error: 'img-scale 缺少 imagePath(inVars[0])' } } if (savePath == null || String(savePath).trim() === '') { return { success: false, error: 'img-scale 缺少 savePath(inVars[1])' } } if (scale === undefined || scale === null || scale === '') { return { success: false, error: 'img-scale 缺少 scale(inVars[2],如 0.8 或 80%)' } } const scaleStr = typeof scale === 'number' && Number.isFinite(scale) ? String(scale) : String(scale).trim() if (!scaleStr) return { success: false, error: 'img-scale scale 无效' } const absIn = buildAbsolutePath(String(imagePath).trim(), folderPath) const absOut = buildAbsolutePath(String(savePath).trim(), folderPath) if (!absIn || !absOut) return { success: false, error: 'img-scale 路径无效' } if (!fs.existsSync(absIn)) return { success: false, error: `img-scale 源文件不存在: ${absIn}` } if (!fs.existsSync(scaleScriptPath)) { return { success: false, error: `脚本不存在: ${scaleScriptPath}` } } const pythonExe = getPythonExeFromConfig(config) const r = spawnSync(pythonExe, [scaleScriptPath, absIn, absOut, scaleStr], { encoding: 'utf-8', timeout: 120_000, cwd: projectRoot, }) const combined = ((r.stdout || '') + '\n' + (r.stderr || '')).trim() if (r.status !== 0) { return { success: false, error: combined || 'img-scale 执行失败' } } let meta = {} const lines = (r.stdout || '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean) const jsonLine = lines.filter((l) => l.startsWith('{')).pop() if (jsonLine) { try { meta = JSON.parse(jsonLine) } catch (_) {} } if (!meta.success) { return { success: false, error: meta.error || combined || 'img-scale 未返回成功' } } return { success: true, path: absOut, value: absOut, result: absOut, scaled: { scale: meta.scale, sourceWidth: meta.sourceWidth, sourceHeight: meta.sourceHeight, width: meta.width, height: meta.height, }, } } module.exports = { tagName, executeImgScale }