@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 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
- // Escape single quotes in text
248
- const escapedText = cleanedText.replace(/'/g, `''`);
249
- // Map text alignment to FFmpeg format (L=left, C=center, R=right)
250
- const textAlignMap = { left: 'L', center: 'C', right: 'R' };
251
- const ffmpegTextAlign = textAlignMap[textAlign] || 'L';
252
- // Build drawtext filter
253
- let drawtext = `drawtext=fontfile=${fontPath}:text='${escapedText}':fontsize=${fontSize}:fontcolor=${fontColor}:x=${positionX}:y=${positionY}:text_align=${ffmpegTextAlign}:line_spacing=${lineSpacing}`;
254
- // Add outline (border) if width > 0
255
- if (outlineWidth > 0) {
256
- drawtext += `:borderw=${outlineWidth}:bordercolor=${outlineColor}`;
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
- // Add background box if enabled
259
- if (enableBackground) {
260
- drawtext += `:box=1:boxcolor=${backgroundColor}@${backgroundOpacity}:boxborderw=${boxPadding}`;
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(drawtext)
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 ffmpegInstaller = require('@ffmpeg-installer/ffmpeg');
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
- const ffmpegInstallerPath = ffmpegInstaller.path;
49
- const ffprobeInstallerPath = ffprobeInstaller.path;
50
- if (ffmpegInstallerPath && fs.existsSync(ffmpegInstallerPath) && ffprobeInstallerPath && fs.existsSync(ffprobeInstallerPath)) {
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 (error) {
72
- // This is the only strategy, so if it fails, we throw.
73
- console.error('Failed to load FFmpeg/FFprobe from node_modules.', error);
74
- throw new n8n_workflow_1.NodeOperationError(
75
- // We can't use `this.getNode()` here as we are in a utility function.
76
- // A generic error is sufficient.
77
- { name: 'MediaFX', type: 'n8n-nodes-mediafx.mediaFX' }, 'Could not load the required FFmpeg executable from the package. ' +
78
- 'This might be due to a restricted execution environment or a broken installation. ' +
79
- 'Please check your n8n environment permissions. ' +
80
- `Original error: ${error.message}`);
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
- // If we get here, something went wrong, but the catch didn't trigger.
83
- console.error('FFmpeg binaries were not found in the expected package path.');
84
- return false;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lee-jisoo/n8n-nodes-mediafx",
3
- "version": "1.6.26",
3
+ "version": "1.6.28",
4
4
  "description": "N8N custom nodes for video editing and media processing (Enhanced fork with Speed control and Subtitle fixes)",
5
5
  "license": "MIT",
6
6
  "author": {