@lee-jisoo/n8n-nodes-mediafx 1.6.24 → 1.6.26
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,25 @@ 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.26
|
|
14
|
+
**New Features**
|
|
15
|
+
|
|
16
|
+
- **📐 Auto Font Size**: Automatically calculate font size based on image dimensions
|
|
17
|
+
- `Auto - Small`: Fits text at 50% of calculated size
|
|
18
|
+
- `Auto - Medium`: Fits text at 75% of calculated size
|
|
19
|
+
- `Auto - Large`: Fits text at 100% (fills available width)
|
|
20
|
+
- Supports Korean/CJK characters with proper width weighting
|
|
21
|
+
|
|
22
|
+
- **😀 Emoji Auto-Removal**: Automatically removes emojis from text
|
|
23
|
+
- Prevents broken/garbled characters in rendered output
|
|
24
|
+
- Emojis are stripped before rendering
|
|
25
|
+
|
|
26
|
+
- **↕️ Line Spacing**: Adjustable spacing between lines
|
|
27
|
+
- Default 10px, configurable for multi-line text
|
|
28
|
+
|
|
29
|
+
- **📝 Multi-line Text Alignment**: Text alignment for multi-line text
|
|
30
|
+
- Left, Center, Right alignment options
|
|
31
|
+
|
|
13
32
|
### v1.6.24
|
|
14
33
|
**Bug Fixes**
|
|
15
34
|
|
|
@@ -414,10 +414,15 @@ class MediaFX {
|
|
|
414
414
|
const { paths, cleanup: c } = await (0, utils_1.resolveInputs)(this, i, [sourceParam.source]);
|
|
415
415
|
cleanup = c;
|
|
416
416
|
const text = this.getNodeParameter('imageText', i, 'Hello, n8n!');
|
|
417
|
+
const sizeMode = this.getNodeParameter('imageTextSizeMode', i, 'fixed');
|
|
417
418
|
const textOptions = {
|
|
418
419
|
fontKey: this.getNodeParameter('imageTextFontKey', i, 'noto-sans-kr'),
|
|
419
|
-
size:
|
|
420
|
+
size: sizeMode === 'fixed'
|
|
421
|
+
? this.getNodeParameter('imageTextSize', i, 48)
|
|
422
|
+
: sizeMode,
|
|
420
423
|
color: this.getNodeParameter('imageTextColor', i, 'white'),
|
|
424
|
+
textAlign: this.getNodeParameter('imageTextAlign', i, 'left'),
|
|
425
|
+
lineSpacing: this.getNodeParameter('imageTextLineSpacing', i, 10),
|
|
421
426
|
outlineWidth: this.getNodeParameter('imageTextOutlineWidth', i, 0),
|
|
422
427
|
outlineColor: this.getNodeParameter('imageTextOutlineColor', i, 'black'),
|
|
423
428
|
enableBackground: this.getNodeParameter('imageTextEnableBackground', i, false),
|
|
@@ -52,6 +52,99 @@ function detectImageFormat(filePath) {
|
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
+
// Get image dimensions using ffprobe
|
|
56
|
+
function getImageDimensions(filePath) {
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
ffmpeg.ffprobe(filePath, (err, metadata) => {
|
|
59
|
+
if (err) {
|
|
60
|
+
return reject(new Error(`Failed to probe image: ${err.message}`));
|
|
61
|
+
}
|
|
62
|
+
const videoStream = metadata.streams.find(s => s.codec_type === 'video');
|
|
63
|
+
if (!videoStream || !videoStream.width || !videoStream.height) {
|
|
64
|
+
return reject(new Error('Could not determine image dimensions'));
|
|
65
|
+
}
|
|
66
|
+
resolve({ width: videoStream.width, height: videoStream.height });
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// Calculate effective text length considering character widths
|
|
71
|
+
// Korean/CJK characters are roughly 2x width of Latin characters
|
|
72
|
+
function calculateEffectiveTextLength(text) {
|
|
73
|
+
let length = 0;
|
|
74
|
+
for (const char of text) {
|
|
75
|
+
const code = char.charCodeAt(0);
|
|
76
|
+
// CJK characters (Korean, Chinese, Japanese)
|
|
77
|
+
if ((code >= 0xAC00 && code <= 0xD7AF) || // Korean Hangul
|
|
78
|
+
(code >= 0x3130 && code <= 0x318F) || // Korean Jamo
|
|
79
|
+
(code >= 0x4E00 && code <= 0x9FFF) || // CJK Unified Ideographs
|
|
80
|
+
(code >= 0x3040 && code <= 0x309F) || // Hiragana
|
|
81
|
+
(code >= 0x30A0 && code <= 0x30FF) // Katakana
|
|
82
|
+
) {
|
|
83
|
+
length += 1.8; // CJK characters are wider
|
|
84
|
+
}
|
|
85
|
+
else if (char === '\n') {
|
|
86
|
+
// Newlines don't contribute to width
|
|
87
|
+
length += 0;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
length += 1; // Latin and other characters
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return length;
|
|
94
|
+
}
|
|
95
|
+
// Remove emojis from text (emojis cause rendering issues with most fonts)
|
|
96
|
+
function removeEmojis(text) {
|
|
97
|
+
// Comprehensive emoji regex pattern covering:
|
|
98
|
+
// - Emoticons, Dingbats, Symbols
|
|
99
|
+
// - Transport, Map, Alchemical symbols
|
|
100
|
+
// - Flags, Skin tone modifiers
|
|
101
|
+
// - ZWJ sequences, Variation selectors
|
|
102
|
+
const emojiPattern = /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F700}-\u{1F77F}]|[\u{1F780}-\u{1F7FF}]|[\u{1F800}-\u{1F8FF}]|[\u{1F900}-\u{1F9FF}]|[\u{1FA00}-\u{1FA6F}]|[\u{1FA70}-\u{1FAFF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{2300}-\u{23FF}]|[\u{2B50}]|[\u{2B55}]|[\u{231A}-\u{231B}]|[\u{23E9}-\u{23F3}]|[\u{23F8}-\u{23FA}]|[\u{25AA}-\u{25AB}]|[\u{25B6}]|[\u{25C0}]|[\u{25FB}-\u{25FE}]|[\u{2614}-\u{2615}]|[\u{2648}-\u{2653}]|[\u{267F}]|[\u{2693}]|[\u{26A1}]|[\u{26AA}-\u{26AB}]|[\u{26BD}-\u{26BE}]|[\u{26C4}-\u{26C5}]|[\u{26CE}]|[\u{26D4}]|[\u{26EA}]|[\u{26F2}-\u{26F3}]|[\u{26F5}]|[\u{26FA}]|[\u{26FD}]|[\u{2702}]|[\u{2705}]|[\u{2708}-\u{270D}]|[\u{270F}]|[\u{2712}]|[\u{2714}]|[\u{2716}]|[\u{271D}]|[\u{2721}]|[\u{2728}]|[\u{2733}-\u{2734}]|[\u{2744}]|[\u{2747}]|[\u{274C}]|[\u{274E}]|[\u{2753}-\u{2755}]|[\u{2757}]|[\u{2763}-\u{2764}]|[\u{2795}-\u{2797}]|[\u{27A1}]|[\u{27B0}]|[\u{27BF}]|[\u{FE00}-\u{FE0F}]|[\u{200D}]|[\u{20E3}]|[\u{E0020}-\u{E007F}]/gu;
|
|
103
|
+
// Remove emojis and clean up any resulting double spaces (but preserve newlines)
|
|
104
|
+
return text
|
|
105
|
+
.replace(emojiPattern, '')
|
|
106
|
+
.split('\n')
|
|
107
|
+
.map(line => line.replace(/ +/g, ' ').trim())
|
|
108
|
+
.join('\n');
|
|
109
|
+
}
|
|
110
|
+
// Calculate auto font size based on image dimensions and text
|
|
111
|
+
function calculateAutoFontSize(imageWidth, imageHeight, text, sizeOption, // 'auto-small', 'auto-medium', 'auto-large'
|
|
112
|
+
paddingX = 20) {
|
|
113
|
+
// Split by newlines and find the longest line
|
|
114
|
+
const lines = text.split('\n');
|
|
115
|
+
let maxLineLength = 0;
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
const lineLength = calculateEffectiveTextLength(line);
|
|
118
|
+
if (lineLength > maxLineLength) {
|
|
119
|
+
maxLineLength = lineLength;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// If empty or only newlines, use a default
|
|
123
|
+
if (maxLineLength === 0) {
|
|
124
|
+
maxLineLength = 1;
|
|
125
|
+
}
|
|
126
|
+
// Available width (with padding on both sides)
|
|
127
|
+
const availableWidth = imageWidth - (paddingX * 2);
|
|
128
|
+
// Base calculation: fit text within available width
|
|
129
|
+
// Approximate: 1 character ≈ 0.6 * fontSize in width for most fonts
|
|
130
|
+
const charWidthRatio = 0.55;
|
|
131
|
+
let baseFontSize = availableWidth / (maxLineLength * charWidthRatio);
|
|
132
|
+
// Apply size multiplier
|
|
133
|
+
const sizeMultipliers = {
|
|
134
|
+
'auto-small': 0.5,
|
|
135
|
+
'auto-medium': 0.75,
|
|
136
|
+
'auto-large': 1.0,
|
|
137
|
+
};
|
|
138
|
+
const multiplier = sizeMultipliers[sizeOption] || 0.75;
|
|
139
|
+
let fontSize = Math.floor(baseFontSize * multiplier);
|
|
140
|
+
// Also consider height constraint (text shouldn't be taller than 80% of image)
|
|
141
|
+
const lineCount = lines.length;
|
|
142
|
+
const maxHeightFontSize = Math.floor((imageHeight * 0.8) / (lineCount * 1.2)); // 1.2 for line spacing
|
|
143
|
+
fontSize = Math.min(fontSize, maxHeightFontSize);
|
|
144
|
+
// Clamp to reasonable range
|
|
145
|
+
fontSize = Math.max(12, Math.min(fontSize, 500));
|
|
146
|
+
return fontSize;
|
|
147
|
+
}
|
|
55
148
|
function getPositionFromAlignment(horizontalAlign, verticalAlign, paddingX, paddingY) {
|
|
56
149
|
let x;
|
|
57
150
|
let y;
|
|
@@ -84,7 +177,7 @@ function getPositionFromAlignment(horizontalAlign, verticalAlign, paddingX, padd
|
|
|
84
177
|
return { x, y };
|
|
85
178
|
}
|
|
86
179
|
async function executeAddTextToImage(imagePath, text, options, itemIndex) {
|
|
87
|
-
var _a, _b, _c;
|
|
180
|
+
var _a, _b, _c, _d;
|
|
88
181
|
// Get font (includes bundled, user, and system fonts)
|
|
89
182
|
const allFonts = (0, utils_1.getAvailableFonts)(true);
|
|
90
183
|
const fontKey = options.fontKey || 'noto-sans-kr';
|
|
@@ -93,16 +186,33 @@ async function executeAddTextToImage(imagePath, text, options, itemIndex) {
|
|
|
93
186
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Selected font '${fontKey}' is not valid or its file path is missing.`, { itemIndex });
|
|
94
187
|
}
|
|
95
188
|
const fontPath = font.path;
|
|
96
|
-
//
|
|
97
|
-
const
|
|
189
|
+
// Remove emojis from text (they cause rendering issues with most fonts)
|
|
190
|
+
const cleanedText = removeEmojis(text);
|
|
191
|
+
// Get padding values early (needed for auto font size calculation)
|
|
192
|
+
const paddingX = (_a = options.paddingX) !== null && _a !== void 0 ? _a : 20;
|
|
193
|
+
const paddingY = (_b = options.paddingY) !== null && _b !== void 0 ? _b : 20;
|
|
194
|
+
// Handle font size - support auto sizing options
|
|
195
|
+
let fontSize;
|
|
196
|
+
const sizeOption = options.size;
|
|
197
|
+
const autoSizeOptions = ['auto-small', 'auto-medium', 'auto-large'];
|
|
198
|
+
if (typeof sizeOption === 'string' && autoSizeOptions.includes(sizeOption)) {
|
|
199
|
+
// Auto font size - need to get image dimensions first
|
|
200
|
+
const dimensions = await getImageDimensions(imagePath);
|
|
201
|
+
fontSize = calculateAutoFontSize(dimensions.width, dimensions.height, cleanedText, sizeOption, paddingX);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
fontSize = sizeOption || 48;
|
|
205
|
+
}
|
|
98
206
|
const fontColor = options.color || 'white';
|
|
207
|
+
const textAlign = options.textAlign || 'left';
|
|
208
|
+
const lineSpacing = (_c = options.lineSpacing) !== null && _c !== void 0 ? _c : 10;
|
|
99
209
|
// Outline options
|
|
100
210
|
const outlineWidth = options.outlineWidth || 0;
|
|
101
211
|
const outlineColor = options.outlineColor || 'black';
|
|
102
212
|
// Background box options
|
|
103
213
|
const enableBackground = options.enableBackground || false;
|
|
104
214
|
const backgroundColor = options.backgroundColor || 'black';
|
|
105
|
-
const backgroundOpacity = (
|
|
215
|
+
const backgroundOpacity = (_d = options.backgroundOpacity) !== null && _d !== void 0 ? _d : 0.5;
|
|
106
216
|
const boxPadding = options.boxPadding || 5;
|
|
107
217
|
// Handle position based on position type
|
|
108
218
|
let positionX;
|
|
@@ -111,8 +221,6 @@ async function executeAddTextToImage(imagePath, text, options, itemIndex) {
|
|
|
111
221
|
if (positionType === 'alignment') {
|
|
112
222
|
const horizontalAlign = options.horizontalAlign || 'center';
|
|
113
223
|
const verticalAlign = options.verticalAlign || 'middle';
|
|
114
|
-
const paddingX = (_b = options.paddingX) !== null && _b !== void 0 ? _b : 20;
|
|
115
|
-
const paddingY = (_c = options.paddingY) !== null && _c !== void 0 ? _c : 20;
|
|
116
224
|
const position = getPositionFromAlignment(horizontalAlign, verticalAlign, paddingX, paddingY);
|
|
117
225
|
positionX = position.x;
|
|
118
226
|
positionY = position.y;
|
|
@@ -137,9 +245,12 @@ async function executeAddTextToImage(imagePath, text, options, itemIndex) {
|
|
|
137
245
|
}
|
|
138
246
|
const outputPath = (0, utils_1.getTempFile)(outputExt);
|
|
139
247
|
// Escape single quotes in text
|
|
140
|
-
const escapedText =
|
|
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';
|
|
141
252
|
// Build drawtext filter
|
|
142
|
-
let drawtext = `drawtext=fontfile=${fontPath}:text='${escapedText}':fontsize=${fontSize}:fontcolor=${fontColor}:x=${positionX}:y=${positionY}`;
|
|
253
|
+
let drawtext = `drawtext=fontfile=${fontPath}:text='${escapedText}':fontsize=${fontSize}:fontcolor=${fontColor}:x=${positionX}:y=${positionY}:text_align=${ffmpegTextAlign}:line_spacing=${lineSpacing}`;
|
|
143
254
|
// Add outline (border) if width > 0
|
|
144
255
|
if (outlineWidth > 0) {
|
|
145
256
|
drawtext += `:borderw=${outlineWidth}:bordercolor=${outlineColor}`;
|
|
@@ -93,6 +93,25 @@ exports.imageProperties = [
|
|
|
93
93
|
},
|
|
94
94
|
description: 'Select a font for the text',
|
|
95
95
|
},
|
|
96
|
+
{
|
|
97
|
+
displayName: 'Font Size Mode',
|
|
98
|
+
name: 'imageTextSizeMode',
|
|
99
|
+
type: 'options',
|
|
100
|
+
options: [
|
|
101
|
+
{ name: 'Fixed (px)', value: 'fixed' },
|
|
102
|
+
{ name: 'Auto - Small', value: 'auto-small', description: 'Automatically fit text, smaller size' },
|
|
103
|
+
{ name: 'Auto - Medium', value: 'auto-medium', description: 'Automatically fit text, medium size' },
|
|
104
|
+
{ name: 'Auto - Large', value: 'auto-large', description: 'Automatically fit text, fills available width' },
|
|
105
|
+
],
|
|
106
|
+
default: 'fixed',
|
|
107
|
+
displayOptions: {
|
|
108
|
+
show: {
|
|
109
|
+
resource: ['image'],
|
|
110
|
+
operation: ['addTextToImage'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
description: 'How to determine font size. Auto modes calculate size based on image dimensions and text length.',
|
|
114
|
+
},
|
|
96
115
|
{
|
|
97
116
|
displayName: 'Font Size',
|
|
98
117
|
name: 'imageTextSize',
|
|
@@ -105,6 +124,7 @@ exports.imageProperties = [
|
|
|
105
124
|
show: {
|
|
106
125
|
resource: ['image'],
|
|
107
126
|
operation: ['addTextToImage'],
|
|
127
|
+
imageTextSizeMode: ['fixed'],
|
|
108
128
|
},
|
|
109
129
|
},
|
|
110
130
|
description: 'Font size in pixels',
|
|
@@ -122,6 +142,40 @@ exports.imageProperties = [
|
|
|
122
142
|
},
|
|
123
143
|
description: 'Text color (e.g., white, #FF0000, rgb(255,0,0))',
|
|
124
144
|
},
|
|
145
|
+
{
|
|
146
|
+
displayName: 'Text Alignment (Multi-line)',
|
|
147
|
+
name: 'imageTextAlign',
|
|
148
|
+
type: 'options',
|
|
149
|
+
options: [
|
|
150
|
+
{ name: 'Left', value: 'left' },
|
|
151
|
+
{ name: 'Center', value: 'center' },
|
|
152
|
+
{ name: 'Right', value: 'right' },
|
|
153
|
+
],
|
|
154
|
+
default: 'left',
|
|
155
|
+
displayOptions: {
|
|
156
|
+
show: {
|
|
157
|
+
resource: ['image'],
|
|
158
|
+
operation: ['addTextToImage'],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
description: 'Text alignment for multi-line text (when text contains line breaks)',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
displayName: 'Line Spacing',
|
|
165
|
+
name: 'imageTextLineSpacing',
|
|
166
|
+
type: 'number',
|
|
167
|
+
default: 10,
|
|
168
|
+
typeOptions: {
|
|
169
|
+
minValue: 0,
|
|
170
|
+
},
|
|
171
|
+
displayOptions: {
|
|
172
|
+
show: {
|
|
173
|
+
resource: ['image'],
|
|
174
|
+
operation: ['addTextToImage'],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
description: 'Additional spacing between lines in pixels (for multi-line text)',
|
|
178
|
+
},
|
|
125
179
|
{
|
|
126
180
|
displayName: 'Outline Width',
|
|
127
181
|
name: 'imageTextOutlineWidth',
|
package/package.json
CHANGED