@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: this.getNodeParameter('imageTextSize', i, 48),
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
- // Set default values for text options
97
- const fontSize = options.size || 48;
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 = (_a = options.backgroundOpacity) !== null && _a !== void 0 ? _a : 0.5;
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 = text.replace(/'/g, `''`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lee-jisoo/n8n-nodes-mediafx",
3
- "version": "1.6.24",
3
+ "version": "1.6.26",
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": {