| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- /**
- * 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 }
|