@lee-jisoo/n8n-nodes-mediafx 1.6.20 → 1.6.21

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,17 @@ 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.21
14
+ **New Features**
15
+
16
+ - **🖼️ Add Text to Image**: Overlay text onto static images
17
+ - Font selection (bundled, user-uploaded, system fonts)
18
+ - Text styling: size, color
19
+ - **Outline**: configurable width and color for text border
20
+ - **Background Box**: optional background with color, opacity, and padding
21
+ - Positioning: alignment-based (9-point grid) or custom X/Y coordinates
22
+ - Output format matches input image format (PNG, JPG, etc.)
23
+
13
24
  ### v1.6.20
14
25
  **New Features**
15
26
 
@@ -81,6 +92,27 @@ npm install @lee-jisoo/n8n-nodes-mediafx
81
92
  RUN cd /home/node/.n8n/nodes && npm install @lee-jisoo/n8n-nodes-mediafx
82
93
  ```
83
94
 
95
+ ## ⚠️ Troubleshooting
96
+
97
+ ### Node not working after upgrade
98
+ If the node doesn't work properly after upgrading to a new version, try **uninstalling and reinstalling** the plugin:
99
+
100
+ **Via n8n Community Nodes:**
101
+ 1. Go to **Settings > Community Nodes**
102
+ 2. Find `@lee-jisoo/n8n-nodes-mediafx` and click **Uninstall**
103
+ 3. Restart n8n
104
+ 4. Go to **Settings > Community Nodes** again
105
+ 5. Click **Install** and enter `@lee-jisoo/n8n-nodes-mediafx`
106
+ 6. Restart n8n
107
+
108
+ **Manual Installation:**
109
+ ```bash
110
+ cd ~/.n8n/nodes
111
+ npm uninstall @lee-jisoo/n8n-nodes-mediafx
112
+ npm install @lee-jisoo/n8n-nodes-mediafx
113
+ # Restart n8n
114
+ ```
115
+
84
116
  ## Features
85
117
 
86
118
  ### Video Operations
@@ -103,6 +135,7 @@ RUN cd /home/node/.n8n/nodes && npm install @lee-jisoo/n8n-nodes-mediafx
103
135
  ### Image Operations
104
136
  | Operation | Description |
105
137
  |-----------|-------------|
138
+ | **Add Text** | Overlay text on image with styling, outline, background box ⭐ NEW |
106
139
  | **Image to Video** | Create video from image with custom duration |
107
140
  | **Stamp Image** | Add watermark with position, size, rotation, opacity, time control |
108
141
 
@@ -126,7 +159,42 @@ RUN cd /home/node/.n8n/nodes && npm install @lee-jisoo/n8n-nodes-mediafx
126
159
 
127
160
  ## Usage Examples
128
161
 
129
- ### Get Media Metadata (New!)
162
+ ### Add Text to Image (New!)
163
+ Overlay text on an image with styling:
164
+ ```json
165
+ {
166
+ "resource": "image",
167
+ "operation": "addTextToImage",
168
+ "sourceImageText": {
169
+ "source": { "sourceType": "binary", "binaryProperty": "data" }
170
+ },
171
+ "imageText": "Hello, World!",
172
+ "imageTextFontKey": "noto-sans-kr",
173
+ "imageTextSize": 48,
174
+ "imageTextColor": "white",
175
+ "imageTextOutlineWidth": 2,
176
+ "imageTextOutlineColor": "black",
177
+ "imageTextPositionType": "alignment",
178
+ "imageTextHorizontalAlign": "center",
179
+ "imageTextVerticalAlign": "bottom",
180
+ "imageTextPaddingY": 50
181
+ }
182
+ ```
183
+
184
+ With background box:
185
+ ```json
186
+ {
187
+ "resource": "image",
188
+ "operation": "addTextToImage",
189
+ "imageText": "Caption Text",
190
+ "imageTextEnableBackground": true,
191
+ "imageTextBackgroundColor": "black",
192
+ "imageTextBackgroundOpacity": 0.7,
193
+ "imageTextBoxPadding": 10
194
+ }
195
+ ```
196
+
197
+ ### Get Media Metadata
130
198
  Extract metadata from video or audio files:
131
199
  ```json
132
200
  {
@@ -406,6 +406,35 @@ class MediaFX {
406
406
  outputPath = await operations_1.executeSingleVideoFade.call(this, paths[0], fadeEffect, fadeStartTime, fadeDuration, outputFormat, i);
407
407
  break;
408
408
  }
409
+ case 'addTextToImage': {
410
+ const sourceParam = this.getNodeParameter('sourceImageText', i, {});
411
+ if (!sourceParam.source) {
412
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Image source is required. Please add an image source.', { itemIndex: i });
413
+ }
414
+ const { paths, cleanup: c } = await (0, utils_1.resolveInputs)(this, i, [sourceParam.source]);
415
+ cleanup = c;
416
+ const text = this.getNodeParameter('imageText', i, 'Hello, n8n!');
417
+ const textOptions = {
418
+ fontKey: this.getNodeParameter('imageTextFontKey', i, 'noto-sans-kr'),
419
+ size: this.getNodeParameter('imageTextSize', i, 48),
420
+ color: this.getNodeParameter('imageTextColor', i, 'white'),
421
+ outlineWidth: this.getNodeParameter('imageTextOutlineWidth', i, 0),
422
+ outlineColor: this.getNodeParameter('imageTextOutlineColor', i, 'black'),
423
+ enableBackground: this.getNodeParameter('imageTextEnableBackground', i, false),
424
+ backgroundColor: this.getNodeParameter('imageTextBackgroundColor', i, 'black'),
425
+ backgroundOpacity: this.getNodeParameter('imageTextBackgroundOpacity', i, 0.5),
426
+ boxPadding: this.getNodeParameter('imageTextBoxPadding', i, 5),
427
+ positionType: this.getNodeParameter('imageTextPositionType', i, 'alignment'),
428
+ horizontalAlign: this.getNodeParameter('imageTextHorizontalAlign', i, 'center'),
429
+ verticalAlign: this.getNodeParameter('imageTextVerticalAlign', i, 'middle'),
430
+ paddingX: this.getNodeParameter('imageTextPaddingX', i, 20),
431
+ paddingY: this.getNodeParameter('imageTextPaddingY', i, 20),
432
+ x: this.getNodeParameter('imageTextX', i, '(w-text_w)/2'),
433
+ y: this.getNodeParameter('imageTextY', i, '(h-text_h)/2'),
434
+ };
435
+ outputPath = await operations_1.executeAddTextToImage.call(this, paths[0], text, textOptions, i);
436
+ break;
437
+ }
409
438
  case 'imageToVideo': {
410
439
  const sourceParam = this.getNodeParameter('sourceImage', i, {});
411
440
  const { paths, cleanup: c } = await (0, utils_1.resolveInputs)(this, i, [sourceParam.source]);
@@ -159,6 +159,7 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
159
159
  // Find timestamp line (may be first or second line)
160
160
  let timestampLine = '';
161
161
  let textStartIndex = 0;
162
+ let inlineText = ''; // Text that may be on the same line as timestamp
162
163
  for (let i = 0; i < lines.length; i++) {
163
164
  if (lines[i].includes('-->')) {
164
165
  timestampLine = lines[i];
@@ -168,15 +169,33 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
168
169
  }
169
170
  if (!timestampLine)
170
171
  continue;
171
- // Parse timestamps: 00:00:01,000 --> 00:00:04,000
172
- const match = timestampLine.match(/(\d{1,2}):(\d{2}):(\d{2})[,.](\d{3})\s*-->\s*(\d{1,2}):(\d{2}):(\d{2})[,.](\d{3})/);
172
+ // Parse timestamps: 00:00:01,000 --> 00:00:04,000 [optional inline text]
173
+ // Extended regex to capture text after timestamp on same line
174
+ const match = timestampLine.match(/(\d{1,2}):(\d{2}):(\d{2})[,.](\d{3})\s*-->\s*(\d{1,2}):(\d{2}):(\d{2})[,.](\d{3})(?:\s+(.*))?/);
173
175
  if (!match)
174
176
  continue;
177
+ // Check if text exists on the same line as timestamp (non-standard SRT format)
178
+ if (match[9] && match[9].trim()) {
179
+ inlineText = match[9].trim();
180
+ }
175
181
  // Convert to ASS time format: H:MM:SS.cc
176
182
  const startTime = `${parseInt(match[1])}:${match[2]}:${match[3]}.${match[4].substring(0, 2)}`;
177
183
  const endTime = `${parseInt(match[5])}:${match[6]}:${match[7]}.${match[8].substring(0, 2)}`;
178
- // Get subtitle text (remaining lines)
179
- let text = lines.slice(textStartIndex).join('\\N');
184
+ // Get subtitle text
185
+ // Priority: inline text (on timestamp line) > remaining lines
186
+ let text;
187
+ if (inlineText) {
188
+ // Non-standard format: text on same line as timestamp
189
+ // Also include any additional lines if present
190
+ const additionalLines = lines.slice(textStartIndex).filter(l => l.trim());
191
+ text = additionalLines.length > 0
192
+ ? [inlineText, ...additionalLines].join('\\N')
193
+ : inlineText;
194
+ }
195
+ else {
196
+ // Standard format: text on separate lines
197
+ text = lines.slice(textStartIndex).join('\\N');
198
+ }
180
199
  // Add horizontal padding when background is enabled
181
200
  if (enableBackground && text.trim()) {
182
201
  text = addTextPadding(text, paddingSpaces);
@@ -0,0 +1,2 @@
1
+ import { IExecuteFunctions, IDataObject } from 'n8n-workflow';
2
+ export declare function executeAddTextToImage(this: IExecuteFunctions, imagePath: string, text: string, options: IDataObject, itemIndex: number): Promise<string>;
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.executeAddTextToImage = void 0;
27
+ const n8n_workflow_1 = require("n8n-workflow");
28
+ const path = __importStar(require("path"));
29
+ const ffmpeg = require("fluent-ffmpeg");
30
+ const utils_1 = require("../utils");
31
+ const fs = __importStar(require("fs-extra"));
32
+ function getPositionFromAlignment(horizontalAlign, verticalAlign, paddingX, paddingY) {
33
+ let x;
34
+ let y;
35
+ // Set X position based on horizontal alignment
36
+ switch (horizontalAlign) {
37
+ case 'left':
38
+ x = `${paddingX}`;
39
+ break;
40
+ case 'right':
41
+ x = `w-text_w-${paddingX}`;
42
+ break;
43
+ case 'center':
44
+ default:
45
+ x = '(w-text_w)/2';
46
+ break;
47
+ }
48
+ // Set Y position based on vertical alignment
49
+ switch (verticalAlign) {
50
+ case 'top':
51
+ y = `${paddingY}`;
52
+ break;
53
+ case 'bottom':
54
+ y = `h-th-${paddingY}`;
55
+ break;
56
+ case 'middle':
57
+ default:
58
+ y = '(h-text_h)/2';
59
+ break;
60
+ }
61
+ return { x, y };
62
+ }
63
+ async function executeAddTextToImage(imagePath, text, options, itemIndex) {
64
+ var _a, _b, _c;
65
+ // Get font (includes bundled, user, and system fonts)
66
+ const allFonts = (0, utils_1.getAvailableFonts)(true);
67
+ const fontKey = options.fontKey || 'noto-sans-kr';
68
+ const font = allFonts[fontKey];
69
+ if (!font || !font.path) {
70
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Selected font '${fontKey}' is not valid or its file path is missing.`, { itemIndex });
71
+ }
72
+ const fontPath = font.path;
73
+ // Set default values for text options
74
+ const fontSize = options.size || 48;
75
+ const fontColor = options.color || 'white';
76
+ // Outline options
77
+ const outlineWidth = options.outlineWidth || 0;
78
+ const outlineColor = options.outlineColor || 'black';
79
+ // Background box options
80
+ const enableBackground = options.enableBackground || false;
81
+ const backgroundColor = options.backgroundColor || 'black';
82
+ const backgroundOpacity = (_a = options.backgroundOpacity) !== null && _a !== void 0 ? _a : 0.5;
83
+ const boxPadding = options.boxPadding || 5;
84
+ // Handle position based on position type
85
+ let positionX;
86
+ let positionY;
87
+ const positionType = options.positionType || 'alignment';
88
+ if (positionType === 'alignment') {
89
+ const horizontalAlign = options.horizontalAlign || 'center';
90
+ const verticalAlign = options.verticalAlign || 'middle';
91
+ const paddingX = (_b = options.paddingX) !== null && _b !== void 0 ? _b : 20;
92
+ const paddingY = (_c = options.paddingY) !== null && _c !== void 0 ? _c : 20;
93
+ const position = getPositionFromAlignment(horizontalAlign, verticalAlign, paddingX, paddingY);
94
+ positionX = position.x;
95
+ positionY = position.y;
96
+ }
97
+ else {
98
+ // Custom position
99
+ positionX = options.x || '(w-text_w)/2';
100
+ positionY = options.y || '(h-text_h)/2';
101
+ }
102
+ // Use the same extension as input image
103
+ const inputExt = path.extname(imagePath);
104
+ const outputPath = (0, utils_1.getTempFile)(inputExt);
105
+ // Escape single quotes in text
106
+ const escapedText = text.replace(/'/g, `''`);
107
+ // Build drawtext filter
108
+ let drawtext = `drawtext=fontfile=${fontPath}:text='${escapedText}':fontsize=${fontSize}:fontcolor=${fontColor}:x=${positionX}:y=${positionY}`;
109
+ // Add outline (border) if width > 0
110
+ if (outlineWidth > 0) {
111
+ drawtext += `:borderw=${outlineWidth}:bordercolor=${outlineColor}`;
112
+ }
113
+ // Add background box if enabled
114
+ if (enableBackground) {
115
+ drawtext += `:box=1:boxcolor=${backgroundColor}@${backgroundOpacity}:boxborderw=${boxPadding}`;
116
+ }
117
+ const command = ffmpeg(imagePath)
118
+ .videoFilters(drawtext)
119
+ .frames(1)
120
+ .save(outputPath);
121
+ try {
122
+ await (0, utils_1.runFfmpeg)(command);
123
+ return outputPath;
124
+ }
125
+ catch (error) {
126
+ // Clean up output file if creation failed
127
+ await fs.remove(outputPath).catch(() => { });
128
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Error adding text to image. FFmpeg error: ${error.message}`, { itemIndex });
129
+ }
130
+ }
131
+ exports.executeAddTextToImage = executeAddTextToImage;
@@ -4,6 +4,7 @@ export * from './speed';
4
4
  export * from './mixAudio';
5
5
  export * from './addSubtitle';
6
6
  export * from './addText';
7
+ export * from './addTextToImage';
7
8
  export * from './extractAudio';
8
9
  export * from './separateAudio';
9
10
  export * from './multiVideoTransition';
@@ -20,6 +20,7 @@ __exportStar(require("./speed"), exports);
20
20
  __exportStar(require("./mixAudio"), exports);
21
21
  __exportStar(require("./addSubtitle"), exports);
22
22
  __exportStar(require("./addText"), exports);
23
+ __exportStar(require("./addTextToImage"), exports);
23
24
  __exportStar(require("./extractAudio"), exports);
24
25
  __exportStar(require("./separateAudio"), exports);
25
26
  __exportStar(require("./multiVideoTransition"), exports);
@@ -14,6 +14,11 @@ exports.imageProperties = [
14
14
  },
15
15
  },
16
16
  options: [
17
+ {
18
+ name: 'Add Text',
19
+ value: 'addTextToImage',
20
+ description: 'Overlay text onto an image',
21
+ },
17
22
  {
18
23
  name: 'To Video',
19
24
  value: 'imageToVideo',
@@ -28,6 +33,299 @@ exports.imageProperties = [
28
33
  default: 'imageToVideo',
29
34
  },
30
35
  // =============================
36
+ // == ADD TEXT TO IMAGE FIELDS ==
37
+ // =============================
38
+ {
39
+ displayName: 'Source Image',
40
+ name: 'sourceImageText',
41
+ type: 'fixedCollection',
42
+ placeholder: 'Add Image Source',
43
+ displayOptions: {
44
+ show: {
45
+ resource: ['image'],
46
+ operation: ['addTextToImage'],
47
+ },
48
+ },
49
+ default: {},
50
+ options: [
51
+ {
52
+ displayName: 'Source',
53
+ name: 'source',
54
+ values: [
55
+ {
56
+ displayName: 'Source Type', name: 'sourceType', type: 'options',
57
+ options: [{ name: 'URL', value: 'url' }, { name: 'Binary Data', value: 'binary' }],
58
+ default: 'url',
59
+ },
60
+ { displayName: 'Value', name: 'value', type: 'string', default: '', placeholder: 'https://example.com/image.png', displayOptions: { show: { sourceType: ['url'] } } },
61
+ { displayName: 'Binary Property', name: 'binaryProperty', type: 'string', default: 'data', displayOptions: { show: { sourceType: ['binary'] } } },
62
+ ],
63
+ },
64
+ ],
65
+ },
66
+ {
67
+ displayName: 'Text',
68
+ name: 'imageText',
69
+ type: 'string',
70
+ default: 'Hello, n8n!',
71
+ required: true,
72
+ displayOptions: {
73
+ show: {
74
+ resource: ['image'],
75
+ operation: ['addTextToImage'],
76
+ },
77
+ },
78
+ description: 'The text to overlay on the image',
79
+ },
80
+ {
81
+ displayName: 'Font',
82
+ name: 'imageTextFontKey',
83
+ type: 'options',
84
+ typeOptions: {
85
+ loadOptionsMethod: 'getFonts',
86
+ },
87
+ default: 'noto-sans-kr',
88
+ displayOptions: {
89
+ show: {
90
+ resource: ['image'],
91
+ operation: ['addTextToImage'],
92
+ },
93
+ },
94
+ description: 'Select a font for the text',
95
+ },
96
+ {
97
+ displayName: 'Font Size',
98
+ name: 'imageTextSize',
99
+ type: 'number',
100
+ default: 48,
101
+ typeOptions: {
102
+ minValue: 1,
103
+ },
104
+ displayOptions: {
105
+ show: {
106
+ resource: ['image'],
107
+ operation: ['addTextToImage'],
108
+ },
109
+ },
110
+ description: 'Font size in pixels',
111
+ },
112
+ {
113
+ displayName: 'Color',
114
+ name: 'imageTextColor',
115
+ type: 'string',
116
+ default: 'white',
117
+ displayOptions: {
118
+ show: {
119
+ resource: ['image'],
120
+ operation: ['addTextToImage'],
121
+ },
122
+ },
123
+ description: 'Text color (e.g., white, #FF0000, rgb(255,0,0))',
124
+ },
125
+ {
126
+ displayName: 'Outline Width',
127
+ name: 'imageTextOutlineWidth',
128
+ type: 'number',
129
+ default: 0,
130
+ typeOptions: {
131
+ minValue: 0,
132
+ },
133
+ displayOptions: {
134
+ show: {
135
+ resource: ['image'],
136
+ operation: ['addTextToImage'],
137
+ },
138
+ },
139
+ description: 'Width of the text outline/border in pixels. Set to 0 for no outline.',
140
+ },
141
+ {
142
+ displayName: 'Outline Color',
143
+ name: 'imageTextOutlineColor',
144
+ type: 'string',
145
+ default: 'black',
146
+ displayOptions: {
147
+ show: {
148
+ resource: ['image'],
149
+ operation: ['addTextToImage'],
150
+ },
151
+ },
152
+ description: 'Color of the text outline/border',
153
+ },
154
+ {
155
+ displayName: 'Enable Background Box',
156
+ name: 'imageTextEnableBackground',
157
+ type: 'boolean',
158
+ default: false,
159
+ displayOptions: {
160
+ show: {
161
+ resource: ['image'],
162
+ operation: ['addTextToImage'],
163
+ },
164
+ },
165
+ description: 'Whether to add a background box behind the text',
166
+ },
167
+ {
168
+ displayName: 'Background Color',
169
+ name: 'imageTextBackgroundColor',
170
+ type: 'string',
171
+ default: 'black',
172
+ displayOptions: {
173
+ show: {
174
+ resource: ['image'],
175
+ operation: ['addTextToImage'],
176
+ imageTextEnableBackground: [true],
177
+ },
178
+ },
179
+ description: 'Color of the background box',
180
+ },
181
+ {
182
+ displayName: 'Background Opacity',
183
+ name: 'imageTextBackgroundOpacity',
184
+ type: 'number',
185
+ default: 0.5,
186
+ typeOptions: {
187
+ minValue: 0,
188
+ maxValue: 1,
189
+ numberStepSize: 0.1,
190
+ },
191
+ displayOptions: {
192
+ show: {
193
+ resource: ['image'],
194
+ operation: ['addTextToImage'],
195
+ imageTextEnableBackground: [true],
196
+ },
197
+ },
198
+ description: 'Opacity of the background box (0 = transparent, 1 = opaque)',
199
+ },
200
+ {
201
+ displayName: 'Box Padding',
202
+ name: 'imageTextBoxPadding',
203
+ type: 'number',
204
+ default: 5,
205
+ typeOptions: {
206
+ minValue: 0,
207
+ },
208
+ displayOptions: {
209
+ show: {
210
+ resource: ['image'],
211
+ operation: ['addTextToImage'],
212
+ imageTextEnableBackground: [true],
213
+ },
214
+ },
215
+ description: 'Padding around the text inside the background box',
216
+ },
217
+ {
218
+ displayName: 'Position Type',
219
+ name: 'imageTextPositionType',
220
+ type: 'options',
221
+ options: [
222
+ { name: 'Alignment', value: 'alignment' },
223
+ { name: 'Custom', value: 'custom' },
224
+ ],
225
+ default: 'alignment',
226
+ displayOptions: {
227
+ show: {
228
+ resource: ['image'],
229
+ operation: ['addTextToImage'],
230
+ },
231
+ },
232
+ description: 'How to position the text on the image',
233
+ },
234
+ {
235
+ displayName: 'Horizontal Alignment',
236
+ name: 'imageTextHorizontalAlign',
237
+ type: 'options',
238
+ options: [
239
+ { name: 'Left', value: 'left' },
240
+ { name: 'Center', value: 'center' },
241
+ { name: 'Right', value: 'right' },
242
+ ],
243
+ default: 'center',
244
+ displayOptions: {
245
+ show: {
246
+ resource: ['image'],
247
+ operation: ['addTextToImage'],
248
+ imageTextPositionType: ['alignment'],
249
+ },
250
+ },
251
+ description: 'Horizontal alignment of the text',
252
+ },
253
+ {
254
+ displayName: 'Vertical Alignment',
255
+ name: 'imageTextVerticalAlign',
256
+ type: 'options',
257
+ options: [
258
+ { name: 'Top', value: 'top' },
259
+ { name: 'Middle', value: 'middle' },
260
+ { name: 'Bottom', value: 'bottom' },
261
+ ],
262
+ default: 'middle',
263
+ displayOptions: {
264
+ show: {
265
+ resource: ['image'],
266
+ operation: ['addTextToImage'],
267
+ imageTextPositionType: ['alignment'],
268
+ },
269
+ },
270
+ description: 'Vertical alignment of the text',
271
+ },
272
+ {
273
+ displayName: 'Padding X',
274
+ name: 'imageTextPaddingX',
275
+ type: 'number',
276
+ default: 20,
277
+ displayOptions: {
278
+ show: {
279
+ resource: ['image'],
280
+ operation: ['addTextToImage'],
281
+ imageTextPositionType: ['alignment'],
282
+ },
283
+ },
284
+ description: 'Horizontal padding from the edge in pixels',
285
+ },
286
+ {
287
+ displayName: 'Padding Y',
288
+ name: 'imageTextPaddingY',
289
+ type: 'number',
290
+ default: 20,
291
+ displayOptions: {
292
+ show: {
293
+ resource: ['image'],
294
+ operation: ['addTextToImage'],
295
+ imageTextPositionType: ['alignment'],
296
+ },
297
+ },
298
+ description: 'Vertical padding from the edge in pixels',
299
+ },
300
+ {
301
+ displayName: 'Position X',
302
+ name: 'imageTextX',
303
+ type: 'string',
304
+ default: '(w-text_w)/2',
305
+ displayOptions: {
306
+ show: {
307
+ resource: ['image'],
308
+ operation: ['addTextToImage'],
309
+ imageTextPositionType: ['custom'],
310
+ },
311
+ },
312
+ description: "X position. Supports FFmpeg expressions like '10', '(w-text_w)/2', 'w-text_w-10'.",
313
+ },
314
+ {
315
+ displayName: 'Position Y',
316
+ name: 'imageTextY',
317
+ type: 'string',
318
+ default: '(h-text_h)/2',
319
+ displayOptions: {
320
+ show: {
321
+ resource: ['image'],
322
+ operation: ['addTextToImage'],
323
+ imageTextPositionType: ['custom'],
324
+ },
325
+ },
326
+ description: "Y position. Supports FFmpeg expressions like '10', '(h-text_h)/2', 'h-th-10'.",
327
+ },
328
+ // =============================
31
329
  // == IMAGE TO VIDEO FIELDS ==
32
330
  // =============================
33
331
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lee-jisoo/n8n-nodes-mediafx",
3
- "version": "1.6.20",
3
+ "version": "1.6.21",
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": {