/** * fun 标签:img-cropping — 按正方形区域居中裁剪图片并保存 * * 入参(inVars 顺序): * - imagePath:源图片路径(相对当前流程目录或绝对路径) * - savePath:输出路径(相对流程目录或绝对路径) * - squareSpec:边长规则 [scale, 轴]: * - [0.8, "w"] / "0.8,w" / "[0.8,w]":边长 = 图片宽度 × 0.8 * - [1, "h"] / "1,h":边长 = 图片高度 × 1 * 轴:w / width / 宽;h / height / 高 * * 若 scale×参照边 大于 min(宽,高),会夹紧到 min(宽,高)。 */ const path = require('path') const fs = require('fs') const { spawnSync } = require('child_process') const { getPythonExeFromConfig } = require('../../../../python-exe-from-config.js') const tagName = 'img-cropping' 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 squareCropScriptPath = path.join(projectRoot, 'python', 'scripts', 'img-crop-square-center.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) } /** @returns {{ scale: number, axis: 'w'|'h' } | { error: string }} */ function parseSquareSpec (raw) { if (raw == null) return { error: 'squareSpec 为空' } if (Array.isArray(raw)) { if (raw.length < 2) return { error: 'squareSpec 数组须为 [scale, 轴],如 [0.8, "w"]' } const scale = Number(raw[0]) const axis = normalizeAxis(raw[1]) if (Number.isNaN(scale) || scale <= 0) return { error: 'squareSpec 中 scale 须为大于 0 的数字' } if (!axis) return { error: 'squareSpec 中轴须为 w 或 h(宽/高)' } return { scale, axis } } const str = typeof raw === 'string' ? raw.trim() : String(raw).trim() if (!str) return { error: 'squareSpec 为空字符串' } try { const j = JSON.parse(str) if (Array.isArray(j) && j.length >= 2) return parseSquareSpec(j) } catch (_) { /* 非 JSON */ } const inner = str.startsWith('[') && str.endsWith(']') ? str.slice(1, -1).trim() : str const parts = inner.split(',').map((s) => s.trim().replace(/^["']|["']$/g, '')) if (parts.length < 2) return { error: 'squareSpec 格式须为 [scale,轴] 或 "scale,轴",如 [0.8,w] 或 1,h' } const scale = Number(parts[0]) const axis = normalizeAxis(parts[1]) if (Number.isNaN(scale) || scale <= 0) return { error: 'scale 须为大于 0 的数字' } if (!axis) return { error: '轴须为 w 或 h' } return { scale, axis } } function normalizeAxis (v) { if (v == null) return null const a = String(v).trim().toLowerCase() if (a === 'w' || a === 'width' || a === '宽') return 'w' if (a === 'h' || a === 'height' || a === '高') return 'h' return null } /** * @param {{ imagePath: string, squareSpec: string|any[], savePath: string, folderPath?: string }} input */ async function executeImgCropping ({ imagePath, squareSpec, savePath, folderPath }) { if (imagePath == null || String(imagePath).trim() === '') { return { success: false, error: 'img-cropping 缺少 imagePath(inVars[0])' } } if (savePath == null || String(savePath).trim() === '') { return { success: false, error: 'img-cropping 缺少 savePath(inVars[1])' } } if (squareSpec === undefined || squareSpec === null || squareSpec === '') { return { success: false, error: 'img-cropping 缺少 squareSpec(inVars[2],如 [0.8,"w"])' } } const spec = parseSquareSpec(squareSpec) if (spec.error) return { success: false, error: `img-cropping squareSpec 无效: ${spec.error}` } const absIn = buildAbsolutePath(String(imagePath).trim(), folderPath) const absOut = buildAbsolutePath(String(savePath).trim(), folderPath) if (!absIn || !absOut) return { success: false, error: 'img-cropping 路径无效' } if (!fs.existsSync(absIn)) return { success: false, error: `img-cropping 源文件不存在: ${absIn}` } if (!fs.existsSync(squareCropScriptPath)) { return { success: false, error: `脚本不存在: ${squareCropScriptPath}` } } const pythonExe = getPythonExeFromConfig(config) const r = spawnSync( pythonExe, [squareCropScriptPath, absIn, absOut, String(spec.scale), spec.axis], { encoding: 'utf-8', timeout: 60000, cwd: projectRoot } ) const combined = ((r.stdout || '') + '\n' + (r.stderr || '')).trim() if (r.status !== 0) { return { success: false, error: combined || 'img-cropping 裁剪失败' } } 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-cropping 未返回成功' } } return { success: true, path: absOut, value: absOut, result: absOut, crop: { x: meta.x, y: meta.y, side: meta.side, sourceWidth: meta.width, sourceHeight: meta.height } } } module.exports = { tagName, executeImgCropping, parseSquareSpec }