@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 +84 -17
- package/dist/nodes/MediaFX/MediaFX.node.js +34 -10
- package/dist/nodes/MediaFX/operations/addSubtitle.js +29 -24
- package/dist/nodes/MediaFX/operations/addText.js +6 -20
- package/dist/nodes/MediaFX/operations/addTextToImage.d.ts +2 -0
- package/dist/nodes/MediaFX/operations/addTextToImage.js +131 -0
- package/dist/nodes/MediaFX/operations/index.d.ts +1 -0
- package/dist/nodes/MediaFX/operations/index.js +1 -0
- package/dist/nodes/MediaFX/properties/image.properties.js +298 -0
- package/dist/nodes/MediaFX/properties/subtitle.properties.js +2 -33
- package/package.json +1 -1
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.
|
|
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
|
-
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
179
|
-
|
|
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 (
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
//
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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,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;
|
|
@@ -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
|
|
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: '
|
|
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