@lee-jisoo/n8n-nodes-mediafx 1.6.26 → 1.6.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/nodes/MediaFX/operations/addTextToImage.js +59 -62
- package/dist/nodes/MediaFX/utils.js +61 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,21 @@ This is a custom n8n node for comprehensive, local media processing using FFmpeg
|
|
|
10
10
|
|
|
11
11
|
## 🆕 What's New in This Fork
|
|
12
12
|
|
|
13
|
+
### v1.6.28
|
|
14
|
+
**New Features**
|
|
15
|
+
|
|
16
|
+
- **📝 Multi-line Center Alignment**: Each line is individually centered
|
|
17
|
+
- Works with older FFmpeg versions (no text_align dependency)
|
|
18
|
+
- Each line rendered as separate drawtext filter
|
|
19
|
+
- Line spacing option supported
|
|
20
|
+
|
|
21
|
+
### v1.6.27
|
|
22
|
+
**Improvements**
|
|
23
|
+
|
|
24
|
+
- **🔧 FFmpeg Bundling Fix**: Now uses `ffmpeg-static` (FFmpeg 5.x) as primary source
|
|
25
|
+
- More reliable cross-platform support
|
|
26
|
+
- Fixes issues with old system FFmpeg being used instead of bundled version
|
|
27
|
+
|
|
13
28
|
### v1.6.26
|
|
14
29
|
**New Features**
|
|
15
30
|
|
|
@@ -145,37 +145,6 @@ paddingX = 20) {
|
|
|
145
145
|
fontSize = Math.max(12, Math.min(fontSize, 500));
|
|
146
146
|
return fontSize;
|
|
147
147
|
}
|
|
148
|
-
function getPositionFromAlignment(horizontalAlign, verticalAlign, paddingX, paddingY) {
|
|
149
|
-
let x;
|
|
150
|
-
let y;
|
|
151
|
-
// Set X position based on horizontal alignment
|
|
152
|
-
switch (horizontalAlign) {
|
|
153
|
-
case 'left':
|
|
154
|
-
x = `${paddingX}`;
|
|
155
|
-
break;
|
|
156
|
-
case 'right':
|
|
157
|
-
x = `w-text_w-${paddingX}`;
|
|
158
|
-
break;
|
|
159
|
-
case 'center':
|
|
160
|
-
default:
|
|
161
|
-
x = '(w-text_w)/2';
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
// Set Y position based on vertical alignment
|
|
165
|
-
switch (verticalAlign) {
|
|
166
|
-
case 'top':
|
|
167
|
-
y = `${paddingY}`;
|
|
168
|
-
break;
|
|
169
|
-
case 'bottom':
|
|
170
|
-
y = `h-th-${paddingY}`;
|
|
171
|
-
break;
|
|
172
|
-
case 'middle':
|
|
173
|
-
default:
|
|
174
|
-
y = '(h-text_h)/2';
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
return { x, y };
|
|
178
|
-
}
|
|
179
148
|
async function executeAddTextToImage(imagePath, text, options, itemIndex) {
|
|
180
149
|
var _a, _b, _c, _d;
|
|
181
150
|
// Get font (includes bundled, user, and system fonts)
|
|
@@ -204,8 +173,8 @@ async function executeAddTextToImage(imagePath, text, options, itemIndex) {
|
|
|
204
173
|
fontSize = sizeOption || 48;
|
|
205
174
|
}
|
|
206
175
|
const fontColor = options.color || 'white';
|
|
207
|
-
const textAlign = options.textAlign || 'left';
|
|
208
176
|
const lineSpacing = (_c = options.lineSpacing) !== null && _c !== void 0 ? _c : 10;
|
|
177
|
+
const horizontalAlign = options.horizontalAlign || 'center';
|
|
209
178
|
// Outline options
|
|
210
179
|
const outlineWidth = options.outlineWidth || 0;
|
|
211
180
|
const outlineColor = options.outlineColor || 'black';
|
|
@@ -214,22 +183,6 @@ async function executeAddTextToImage(imagePath, text, options, itemIndex) {
|
|
|
214
183
|
const backgroundColor = options.backgroundColor || 'black';
|
|
215
184
|
const backgroundOpacity = (_d = options.backgroundOpacity) !== null && _d !== void 0 ? _d : 0.5;
|
|
216
185
|
const boxPadding = options.boxPadding || 5;
|
|
217
|
-
// Handle position based on position type
|
|
218
|
-
let positionX;
|
|
219
|
-
let positionY;
|
|
220
|
-
const positionType = options.positionType || 'alignment';
|
|
221
|
-
if (positionType === 'alignment') {
|
|
222
|
-
const horizontalAlign = options.horizontalAlign || 'center';
|
|
223
|
-
const verticalAlign = options.verticalAlign || 'middle';
|
|
224
|
-
const position = getPositionFromAlignment(horizontalAlign, verticalAlign, paddingX, paddingY);
|
|
225
|
-
positionX = position.x;
|
|
226
|
-
positionY = position.y;
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
// Custom position
|
|
230
|
-
positionX = options.x || '(w-text_w)/2';
|
|
231
|
-
positionY = options.y || '(h-text_h)/2';
|
|
232
|
-
}
|
|
233
186
|
// Determine output extension - detect actual format if input has .tmp extension
|
|
234
187
|
let outputExt = path.extname(imagePath).toLowerCase();
|
|
235
188
|
const knownImageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.tiff'];
|
|
@@ -244,23 +197,67 @@ async function executeAddTextToImage(imagePath, text, options, itemIndex) {
|
|
|
244
197
|
}
|
|
245
198
|
}
|
|
246
199
|
const outputPath = (0, utils_1.getTempFile)(outputExt);
|
|
247
|
-
//
|
|
248
|
-
const
|
|
249
|
-
//
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
// Build drawtext
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
200
|
+
// Split text into lines for multi-line center alignment support
|
|
201
|
+
const lines = cleanedText.split('\n').filter(line => line.length > 0);
|
|
202
|
+
// Calculate line height (fontSize + lineSpacing)
|
|
203
|
+
const lineHeight = fontSize + lineSpacing;
|
|
204
|
+
const totalTextHeight = lines.length * fontSize + (lines.length - 1) * lineSpacing;
|
|
205
|
+
// Build drawtext filters - one for each line
|
|
206
|
+
const drawtextFilters = [];
|
|
207
|
+
for (let i = 0; i < lines.length; i++) {
|
|
208
|
+
const line = lines[i];
|
|
209
|
+
const escapedLine = line.replace(/'/g, `''`);
|
|
210
|
+
// Calculate X position based on horizontal alignment
|
|
211
|
+
let lineX;
|
|
212
|
+
switch (horizontalAlign) {
|
|
213
|
+
case 'left':
|
|
214
|
+
lineX = `${paddingX}`;
|
|
215
|
+
break;
|
|
216
|
+
case 'right':
|
|
217
|
+
lineX = `w-text_w-${paddingX}`;
|
|
218
|
+
break;
|
|
219
|
+
case 'center':
|
|
220
|
+
default:
|
|
221
|
+
lineX = '(w-text_w)/2';
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
// Calculate Y position for this line
|
|
225
|
+
// Base Y from vertical alignment, then offset by line index
|
|
226
|
+
let lineY;
|
|
227
|
+
const verticalAlign = options.verticalAlign || 'middle';
|
|
228
|
+
const lineOffset = i * lineHeight;
|
|
229
|
+
switch (verticalAlign) {
|
|
230
|
+
case 'top':
|
|
231
|
+
lineY = `${paddingY + lineOffset}`;
|
|
232
|
+
break;
|
|
233
|
+
case 'bottom':
|
|
234
|
+
// Start from bottom, going up for total height, then down for each line
|
|
235
|
+
lineY = `h-${paddingY + totalTextHeight - lineOffset}`;
|
|
236
|
+
break;
|
|
237
|
+
case 'middle':
|
|
238
|
+
default:
|
|
239
|
+
// Center vertically, then offset each line
|
|
240
|
+
lineY = `(h-${totalTextHeight})/2+${lineOffset}`;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
// Build drawtext for this line
|
|
244
|
+
let drawtext = `drawtext=fontfile=${fontPath}:text='${escapedLine}':fontsize=${fontSize}:fontcolor=${fontColor}:x=${lineX}:y=${lineY}`;
|
|
245
|
+
// Add outline (border) if width > 0
|
|
246
|
+
if (outlineWidth > 0) {
|
|
247
|
+
drawtext += `:borderw=${outlineWidth}:bordercolor=${outlineColor}`;
|
|
248
|
+
}
|
|
249
|
+
// Add background box if enabled
|
|
250
|
+
if (enableBackground) {
|
|
251
|
+
drawtext += `:box=1:boxcolor=${backgroundColor}@${backgroundOpacity}:boxborderw=${boxPadding}`;
|
|
252
|
+
}
|
|
253
|
+
drawtextFilters.push(drawtext);
|
|
257
254
|
}
|
|
258
|
-
//
|
|
259
|
-
if (
|
|
260
|
-
drawtext
|
|
255
|
+
// If no lines (empty text), create a single empty filter to avoid errors
|
|
256
|
+
if (drawtextFilters.length === 0) {
|
|
257
|
+
drawtextFilters.push(`drawtext=fontfile=${fontPath}:text='':fontsize=${fontSize}:fontcolor=${fontColor}:x=0:y=0`);
|
|
261
258
|
}
|
|
262
259
|
const command = ffmpeg(imagePath)
|
|
263
|
-
.videoFilters(
|
|
260
|
+
.videoFilters(drawtextFilters)
|
|
264
261
|
.frames(1)
|
|
265
262
|
.save(outputPath);
|
|
266
263
|
try {
|
|
@@ -40,48 +40,74 @@ let ffmpegInitialized = false;
|
|
|
40
40
|
function tryInitializeFfmpeg() {
|
|
41
41
|
if (ffmpegInitialized)
|
|
42
42
|
return true;
|
|
43
|
+
let ffmpegBinaryPath = null;
|
|
44
|
+
let ffprobeBinaryPath = null;
|
|
45
|
+
// Strategy 1: Try ffmpeg-static (recommended, includes FFmpeg 5.x+)
|
|
43
46
|
try {
|
|
44
47
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
45
|
-
const
|
|
48
|
+
const ffmpegStatic = require('ffmpeg-static');
|
|
49
|
+
if (ffmpegStatic && fs.existsSync(ffmpegStatic)) {
|
|
50
|
+
ffmpegBinaryPath = ffmpegStatic;
|
|
51
|
+
console.log(`FFmpeg found via ffmpeg-static: ${ffmpegStatic}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
console.warn('ffmpeg-static not available, trying fallback...');
|
|
56
|
+
}
|
|
57
|
+
// Strategy 2: Fallback to @ffmpeg-installer/ffmpeg
|
|
58
|
+
if (!ffmpegBinaryPath) {
|
|
59
|
+
try {
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
61
|
+
const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg');
|
|
62
|
+
if (ffmpegInstaller.path && fs.existsSync(ffmpegInstaller.path)) {
|
|
63
|
+
ffmpegBinaryPath = ffmpegInstaller.path;
|
|
64
|
+
console.log(`FFmpeg found via @ffmpeg-installer: ${ffmpegInstaller.path}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
console.warn('@ffmpeg-installer/ffmpeg not available');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Get ffprobe from @ffprobe-installer/ffprobe
|
|
72
|
+
try {
|
|
46
73
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
47
74
|
const ffprobeInstaller = require('@ffprobe-installer/ffprobe');
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// Set executable permissions dynamically
|
|
52
|
-
if (os.platform() !== 'win32') {
|
|
53
|
-
try {
|
|
54
|
-
fs.chmodSync(ffmpegInstallerPath, '755');
|
|
55
|
-
fs.chmodSync(ffprobeInstallerPath, '755');
|
|
56
|
-
console.log('Dynamically set permissions for ffmpeg and ffprobe.');
|
|
57
|
-
}
|
|
58
|
-
catch (permissionError) {
|
|
59
|
-
console.warn('Failed to set executable permissions dynamically:', permissionError);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
ffmpeg.setFfmpegPath(ffmpegInstallerPath);
|
|
63
|
-
ffmpeg.setFfprobePath(ffprobeInstallerPath);
|
|
64
|
-
ffmpegPath = ffmpegInstallerPath;
|
|
65
|
-
ffmpegInitialized = true;
|
|
66
|
-
console.log(`FFmpeg initialized with @ffmpeg-installer: ${ffmpegInstallerPath}`);
|
|
67
|
-
console.log(`FFprobe initialized with @ffprobe-installer: ${ffprobeInstallerPath}`);
|
|
68
|
-
return true;
|
|
75
|
+
if (ffprobeInstaller.path && fs.existsSync(ffprobeInstaller.path)) {
|
|
76
|
+
ffprobeBinaryPath = ffprobeInstaller.path;
|
|
77
|
+
console.log(`FFprobe found via @ffprobe-installer: ${ffprobeInstaller.path}`);
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
|
-
catch (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
{ name: 'MediaFX', type: 'n8n-nodes-mediafx.mediaFX' }, 'Could not
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
catch (e) {
|
|
81
|
+
console.warn('@ffprobe-installer/ffprobe not available');
|
|
82
|
+
}
|
|
83
|
+
// Validate and set paths
|
|
84
|
+
if (!ffmpegBinaryPath) {
|
|
85
|
+
console.error('FFmpeg binary not found in any package.');
|
|
86
|
+
throw new n8n_workflow_1.NodeOperationError({ name: 'MediaFX', type: 'n8n-nodes-mediafx.mediaFX' }, 'Could not find FFmpeg executable. Please ensure ffmpeg-static or @ffmpeg-installer/ffmpeg is properly installed.');
|
|
87
|
+
}
|
|
88
|
+
if (!ffprobeBinaryPath) {
|
|
89
|
+
console.error('FFprobe binary not found.');
|
|
90
|
+
throw new n8n_workflow_1.NodeOperationError({ name: 'MediaFX', type: 'n8n-nodes-mediafx.mediaFX' }, 'Could not find FFprobe executable. Please ensure @ffprobe-installer/ffprobe is properly installed.');
|
|
81
91
|
}
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
// Set executable permissions on non-Windows systems
|
|
93
|
+
if (os.platform() !== 'win32') {
|
|
94
|
+
try {
|
|
95
|
+
fs.chmodSync(ffmpegBinaryPath, '755');
|
|
96
|
+
fs.chmodSync(ffprobeBinaryPath, '755');
|
|
97
|
+
console.log('Set executable permissions for ffmpeg and ffprobe.');
|
|
98
|
+
}
|
|
99
|
+
catch (permissionError) {
|
|
100
|
+
console.warn('Failed to set executable permissions:', permissionError);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Configure fluent-ffmpeg
|
|
104
|
+
ffmpeg.setFfmpegPath(ffmpegBinaryPath);
|
|
105
|
+
ffmpeg.setFfprobePath(ffprobeBinaryPath);
|
|
106
|
+
ffmpegPath = ffmpegBinaryPath;
|
|
107
|
+
ffmpegInitialized = true;
|
|
108
|
+
console.log(`FFmpeg initialized: ${ffmpegBinaryPath}`);
|
|
109
|
+
console.log(`FFprobe initialized: ${ffprobeBinaryPath}`);
|
|
110
|
+
return true;
|
|
85
111
|
}
|
|
86
112
|
// Try to initialize FFmpeg on module load
|
|
87
113
|
tryInitializeFfmpeg();
|
package/package.json
CHANGED