@lee-jisoo/n8n-nodes-mediafx 1.6.19 → 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,7 +10,18 @@ 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.19
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
+
24
+ ### v1.6.20
14
25
  **New Features**
15
26
 
16
27
  - **🔍 Get Metadata (Probe)**: Extract comprehensive metadata from video/audio files
@@ -20,9 +31,9 @@ This is a custom n8n node for comprehensive, local media processing using FFmpeg
20
31
  - Tags: title, artist, album, and other embedded metadata
21
32
 
22
33
  - **🔤 System Font Support**: Use fonts installed on your system
23
- - Scan system fonts with Font > List operation (enable "Include System Fonts")
24
- - Directly specify system font path in Text/Subtitle operations
34
+ - System fonts automatically appear in Font dropdown (Text/Subtitle operations)
25
35
  - Supports macOS, Linux, and Windows system font directories
36
+ - Use Font > List with "Include System Fonts" to browse all available fonts
26
37
 
27
38
  ### v1.6.14
28
39
  **Subtitle Enhancements (v1.6.1 ~ v1.6.14)**
@@ -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
  {
@@ -170,7 +238,18 @@ Example output:
170
238
 
171
239
  ### Using System Fonts (New!)
172
240
 
173
- First, list available system fonts:
241
+ System fonts are automatically available in the Font dropdown. Just select any font with `(system)` suffix:
242
+ ```json
243
+ {
244
+ "resource": "subtitle",
245
+ "operation": "addSubtitle",
246
+ "fontKey": "system-helvetica",
247
+ "size": 48,
248
+ "color": "white"
249
+ }
250
+ ```
251
+
252
+ To browse all system fonts, use Font > List:
174
253
  ```json
175
254
  {
176
255
  "resource": "font",
@@ -182,18 +261,6 @@ First, list available system fonts:
182
261
  }
183
262
  ```
184
263
 
185
- Add subtitles with a system font:
186
- ```json
187
- {
188
- "resource": "subtitle",
189
- "operation": "addSubtitle",
190
- "fontSource": "system",
191
- "systemFontPath": "/System/Library/Fonts/Helvetica.ttc",
192
- "size": 48,
193
- "color": "white"
194
- }
195
- ```
196
-
197
264
  ### Speed Adjustment
198
265
  Create a 2x speed video:
199
266
  ```json
@@ -66,10 +66,11 @@ class MediaFX {
66
66
  };
67
67
  this.methods = {
68
68
  loadOptions: {
69
- // Load available fonts from API
69
+ // Load available fonts from API (including system fonts)
70
70
  async getFonts() {
71
71
  try {
72
- const allFonts = (0, utils_1.getAvailableFonts)();
72
+ // Include system fonts in the dropdown
73
+ const allFonts = (0, utils_1.getAvailableFonts)(true);
73
74
  return Object.entries(allFonts).map(([key, font]) => ({
74
75
  name: `${font.name || key} (${font.type})`,
75
76
  value: key,
@@ -336,11 +337,8 @@ class MediaFX {
336
337
  await subFileCleanup();
337
338
  };
338
339
  // Collect style options from individual parameters
339
- const fontSource = this.getNodeParameter('fontSource', i, 'bundled');
340
340
  const style = {
341
- fontSource,
342
- fontKey: fontSource === 'bundled' ? this.getNodeParameter('fontKey', i, 'noto-sans-kr') : undefined,
343
- systemFontPath: fontSource === 'system' ? this.getNodeParameter('systemFontPath', i, '') : undefined,
341
+ fontKey: this.getNodeParameter('fontKey', i, 'noto-sans-kr'),
344
342
  size: this.getNodeParameter('size', i, 48),
345
343
  color: this.getNodeParameter('color', i, 'white'),
346
344
  outlineWidth: this.getNodeParameter('outlineWidth', i, 1),
@@ -368,11 +366,8 @@ class MediaFX {
368
366
  const startTime = this.getNodeParameter('startTime', i, 0);
369
367
  const endTime = this.getNodeParameter('endTime', i, 5);
370
368
  // Collect style options from individual parameters
371
- const textFontSource = this.getNodeParameter('fontSource', i, 'bundled');
372
369
  const textOptions = {
373
- fontSource: textFontSource,
374
- fontKey: textFontSource === 'bundled' ? this.getNodeParameter('fontKey', i, 'noto-sans-kr') : undefined,
375
- systemFontPath: textFontSource === 'system' ? this.getNodeParameter('systemFontPath', i, '') : undefined,
370
+ fontKey: this.getNodeParameter('fontKey', i, 'noto-sans-kr'),
376
371
  size: this.getNodeParameter('size', i, 48),
377
372
  color: this.getNodeParameter('color', i, 'white'),
378
373
  outlineWidth: this.getNodeParameter('outlineWidth', i, 1),
@@ -411,6 +406,35 @@ class MediaFX {
411
406
  outputPath = await operations_1.executeSingleVideoFade.call(this, paths[0], fadeEffect, fadeStartTime, fadeDuration, outputFormat, i);
412
407
  break;
413
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
+ }
414
438
  case 'imageToVideo': {
415
439
  const sourceParam = this.getNodeParameter('sourceImage', i, {});
416
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);
@@ -190,26 +209,12 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
190
209
  async function executeAddSubtitle(video, subtitleFile, style, itemIndex) {
191
210
  var _a, _b, _c, _d, _e;
192
211
  const outputPath = (0, utils_1.getTempFile)(path.extname(video));
193
- // 1. Get Font (support both bundled and system fonts)
194
- const fontSource = style.fontSource || 'bundled';
195
- let font = null;
196
- if (fontSource === 'system') {
197
- const systemFontPath = style.systemFontPath;
198
- if (!systemFontPath) {
199
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'System font path is required when using system fonts.', { itemIndex });
200
- }
201
- font = (0, utils_1.getFontByPath)(systemFontPath);
202
- if (!font) {
203
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `System font not found at path '${systemFontPath}'. Please check the path and ensure it's a valid font file (TTF, OTF, TTC).`, { itemIndex });
204
- }
205
- }
206
- else {
207
- const allFonts = (0, utils_1.getAvailableFonts)();
208
- const fontKey = style.fontKey || 'noto-sans-kr';
209
- font = allFonts[fontKey] || null;
210
- if (!font || !font.path) {
211
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Selected font key '${fontKey}' is not valid or its file path is missing.`, { itemIndex });
212
- }
212
+ // 1. Get Font (includes bundled, user, and system fonts)
213
+ const allFonts = (0, utils_1.getAvailableFonts)(true);
214
+ const fontKey = style.fontKey || 'noto-sans-kr';
215
+ const font = allFonts[fontKey];
216
+ if (!font || !font.path) {
217
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Selected font '${fontKey}' is not valid or its file path is missing.`, { itemIndex });
213
218
  }
214
219
  const fontName = font.name || 'Sans';
215
220
  const fontSize = style.size || 48;
@@ -62,26 +62,12 @@ function getPositionFromAlignment(horizontalAlign, verticalAlign, paddingX, padd
62
62
  }
63
63
  async function executeAddText(video, text, options, itemIndex) {
64
64
  var _a, _b, _c, _d;
65
- // Support both bundled and system fonts
66
- const fontSource = options.fontSource || 'bundled';
67
- let font = null;
68
- if (fontSource === 'system') {
69
- const systemFontPath = options.systemFontPath;
70
- if (!systemFontPath) {
71
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'System font path is required when using system fonts.', { itemIndex });
72
- }
73
- font = (0, utils_1.getFontByPath)(systemFontPath);
74
- if (!font) {
75
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `System font not found at path '${systemFontPath}'. Please check the path and ensure it's a valid font file (TTF, OTF, TTC).`, { itemIndex });
76
- }
77
- }
78
- else {
79
- const allFonts = (0, utils_1.getAvailableFonts)();
80
- const fontKey = options.fontKey || 'noto-sans-kr';
81
- font = allFonts[fontKey] || null;
82
- if (!font || !font.path) {
83
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Selected font key '${fontKey}' is not valid or its file path is missing.`, { itemIndex });
84
- }
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 });
85
71
  }
86
72
  const fontPath = font.path;
87
73
  // Set default values for text options
@@ -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
  {
@@ -130,23 +130,7 @@ exports.subtitleProperties = [
130
130
  // COMMON FONT & STYLE OPTIONS
131
131
  // ===================
132
132
  {
133
- displayName: 'Font Source',
134
- name: 'fontSource',
135
- type: 'options',
136
- options: [
137
- { name: 'Bundled/Uploaded Fonts', value: 'bundled', description: 'Use fonts bundled with MediaFX or uploaded by user' },
138
- { name: 'System Font Path', value: 'system', description: 'Specify path to a system font file' },
139
- ],
140
- default: 'bundled',
141
- displayOptions: {
142
- show: {
143
- resource: ['subtitle'],
144
- },
145
- },
146
- description: 'Choose font source type',
147
- },
148
- {
149
- displayName: 'Font Key',
133
+ displayName: 'Font',
150
134
  name: 'fontKey',
151
135
  type: 'options',
152
136
  typeOptions: { loadOptionsMethod: 'getFonts' },
@@ -154,24 +138,9 @@ exports.subtitleProperties = [
154
138
  displayOptions: {
155
139
  show: {
156
140
  resource: ['subtitle'],
157
- fontSource: ['bundled'],
158
- },
159
- },
160
- description: 'Font to use for the text',
161
- },
162
- {
163
- displayName: 'System Font Path',
164
- name: 'systemFontPath',
165
- type: 'string',
166
- default: '',
167
- placeholder: '/System/Library/Fonts/AppleSDGothicNeo.ttc',
168
- displayOptions: {
169
- show: {
170
- resource: ['subtitle'],
171
- fontSource: ['system'],
172
141
  },
173
142
  },
174
- description: 'Full path to system font file (TTF, OTF, TTC). Use Font > List operation with "Include System Fonts" to find available fonts.',
143
+ description: 'Font to use for the text (includes bundled, user-uploaded, and system fonts)',
175
144
  },
176
145
  {
177
146
  displayName: 'Font Size',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lee-jisoo/n8n-nodes-mediafx",
3
- "version": "1.6.19",
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": {