@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.25
13
+ ### v1.6.26
14
14
  **New Features**
15
15
 
16
- - **📝 Multi-line Text Alignment**: Added text alignment option for Add Text to Image
17
- - Left, Center, Right alignment for multi-line text
18
- - Useful when text contains line breaks (`\n`)
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: 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'),
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
- // 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';
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 = (_a = options.backgroundOpacity) !== null && _a !== void 0 ? _a : 0.5;
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 = text.replace(/'/g, `''`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lee-jisoo/n8n-nodes-mediafx",
3
- "version": "1.6.25",
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": {