@lee-jisoo/n8n-nodes-mediafx 1.6.25 → 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,12 +10,24 @@ 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.26
|
|
14
14
|
**New Features**
|
|
15
15
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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
|
|
19
31
|
|
|
20
32
|
### v1.6.24
|
|
21
33
|
**Bug Fixes**
|
|
@@ -414,11 +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'),
|
|
421
424
|
textAlign: this.getNodeParameter('imageTextAlign', i, 'left'),
|
|
425
|
+
lineSpacing: this.getNodeParameter('imageTextLineSpacing', i, 10),
|
|
422
426
|
outlineWidth: this.getNodeParameter('imageTextOutlineWidth', i, 0),
|
|
423
427
|
outlineColor: this.getNodeParameter('imageTextOutlineColor', i, 'black'),
|
|
424
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,17 +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';
|
|
99
207
|
const textAlign = options.textAlign || 'left';
|
|
208
|
+
const lineSpacing = (_c = options.lineSpacing) !== null && _c !== void 0 ? _c : 10;
|
|
100
209
|
// Outline options
|
|
101
210
|
const outlineWidth = options.outlineWidth || 0;
|
|
102
211
|
const outlineColor = options.outlineColor || 'black';
|
|
103
212
|
// Background box options
|
|
104
213
|
const enableBackground = options.enableBackground || false;
|
|
105
214
|
const backgroundColor = options.backgroundColor || 'black';
|
|
106
|
-
const backgroundOpacity = (
|
|
215
|
+
const backgroundOpacity = (_d = options.backgroundOpacity) !== null && _d !== void 0 ? _d : 0.5;
|
|
107
216
|
const boxPadding = options.boxPadding || 5;
|
|
108
217
|
// Handle position based on position type
|
|
109
218
|
let positionX;
|
|
@@ -112,8 +221,6 @@ async function executeAddTextToImage(imagePath, text, options, itemIndex) {
|
|
|
112
221
|
if (positionType === 'alignment') {
|
|
113
222
|
const horizontalAlign = options.horizontalAlign || 'center';
|
|
114
223
|
const verticalAlign = options.verticalAlign || 'middle';
|
|
115
|
-
const paddingX = (_b = options.paddingX) !== null && _b !== void 0 ? _b : 20;
|
|
116
|
-
const paddingY = (_c = options.paddingY) !== null && _c !== void 0 ? _c : 20;
|
|
117
224
|
const position = getPositionFromAlignment(horizontalAlign, verticalAlign, paddingX, paddingY);
|
|
118
225
|
positionX = position.x;
|
|
119
226
|
positionY = position.y;
|
|
@@ -138,12 +245,12 @@ async function executeAddTextToImage(imagePath, text, options, itemIndex) {
|
|
|
138
245
|
}
|
|
139
246
|
const outputPath = (0, utils_1.getTempFile)(outputExt);
|
|
140
247
|
// Escape single quotes in text
|
|
141
|
-
const escapedText =
|
|
248
|
+
const escapedText = cleanedText.replace(/'/g, `''`);
|
|
142
249
|
// Map text alignment to FFmpeg format (L=left, C=center, R=right)
|
|
143
250
|
const textAlignMap = { left: 'L', center: 'C', right: 'R' };
|
|
144
251
|
const ffmpegTextAlign = textAlignMap[textAlign] || 'L';
|
|
145
252
|
// Build drawtext filter
|
|
146
|
-
let drawtext = `drawtext=fontfile=${fontPath}:text='${escapedText}':fontsize=${fontSize}:fontcolor=${fontColor}:x=${positionX}:y=${positionY}:text_align=${ffmpegTextAlign}`;
|
|
253
|
+
let drawtext = `drawtext=fontfile=${fontPath}:text='${escapedText}':fontsize=${fontSize}:fontcolor=${fontColor}:x=${positionX}:y=${positionY}:text_align=${ffmpegTextAlign}:line_spacing=${lineSpacing}`;
|
|
147
254
|
// Add outline (border) if width > 0
|
|
148
255
|
if (outlineWidth > 0) {
|
|
149
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',
|
|
@@ -140,6 +160,22 @@ exports.imageProperties = [
|
|
|
140
160
|
},
|
|
141
161
|
description: 'Text alignment for multi-line text (when text contains line breaks)',
|
|
142
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
|
+
},
|
|
143
179
|
{
|
|
144
180
|
displayName: 'Outline Width',
|
|
145
181
|
name: 'imageTextOutlineWidth',
|
package/package.json
CHANGED