figma-metadata-extractor 1.0.5 → 1.0.7

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
@@ -135,10 +135,14 @@ Downloads SVG and PNG images from a Figma file.
135
135
 
136
136
  **Additional Options:**
137
137
  - `pngScale?: number` - Export scale for PNG images (default: 2)
138
- - `localPath: string` - Absolute path to save images
138
+ - `localPath?: string` - Absolute path to save images (optional if returnBuffer is true)
139
+ - `returnBuffer?: boolean` - Return images as ArrayBuffer instead of saving to disk (default: false)
140
+ - `enableLogging?: boolean` - Enable JSON debug log files (default: false)
139
141
 
140
142
  **Returns:** Promise<FigmaImageResult[]>
141
143
 
144
+ When `returnBuffer` is true, each result will contain a `buffer` property instead of `filePath`.
145
+
142
146
  ### `downloadFigmaFrameImage(figmaUrl, options)`
143
147
 
144
148
  Downloads a single frame image from a Figma URL that contains a node-id parameter.
@@ -151,13 +155,22 @@ Downloads a single frame image from a Figma URL that contains a node-id paramete
151
155
  - `apiKey?: string` - Figma API key (Personal Access Token)
152
156
  - `oauthToken?: string` - Figma OAuth Bearer token
153
157
  - `useOAuth?: boolean` - Whether to use OAuth instead of API key
154
- - `localPath: string` - Absolute path to save the image
155
- - `fileName: string` - Local filename (must end with .png or .svg)
158
+ - `localPath?: string` - Absolute path to save the image (optional if returnBuffer is true)
159
+ - `fileName?: string` - Local filename (must end with .png or .svg, optional if returnBuffer is true)
156
160
  - `format?: 'png' | 'svg'` - Image format to download (default: 'png')
157
161
  - `pngScale?: number` - Export scale for PNG images (default: 2)
162
+ - `returnBuffer?: boolean` - Return image as ArrayBuffer instead of saving to disk (default: false)
163
+ - `enableLogging?: boolean` - Enable JSON debug log files (default: false)
158
164
 
159
165
  **Returns:** Promise<FigmaImageResult>
160
166
 
167
+ **Result Properties:**
168
+ - `filePath?: string` - Path to saved file (only when returnBuffer is false)
169
+ - `buffer?: ArrayBuffer` - Image data as ArrayBuffer (only when returnBuffer is true)
170
+ - `finalDimensions: { width: number; height: number }` - Image dimensions
171
+ - `wasCropped: boolean` - Whether the image was cropped
172
+ - `cssVariables?: string` - CSS variables for dimensions (if requested)
173
+
161
174
  ## Authentication
162
175
 
163
176
  You need either a Figma API key or OAuth token:
@@ -184,6 +197,7 @@ import { downloadFigmaFrameImage } from 'figma-metadata-extractor';
184
197
  // Copy this URL from Figma when viewing a frame
185
198
  const figmaUrl = 'https://www.figma.com/design/ABC123/My-Design?node-id=1234-5678&t=xyz123';
186
199
 
200
+ // Save to disk
187
201
  const result = await downloadFigmaFrameImage(figmaUrl, {
188
202
  apiKey: 'your-figma-api-key',
189
203
  localPath: './downloads',
@@ -196,6 +210,29 @@ console.log(`Downloaded to: ${result.filePath}`);
196
210
  console.log(`Dimensions: ${result.finalDimensions.width}x${result.finalDimensions.height}`);
197
211
  ```
198
212
 
213
+ ### Get Frame Image as ArrayBuffer (No Disk Write)
214
+
215
+ If you want to process the image in memory without saving to disk:
216
+
217
+ ```typescript
218
+ import { downloadFigmaFrameImage } from 'figma-metadata-extractor';
219
+
220
+ const figmaUrl = 'https://www.figma.com/design/ABC123/My-Design?node-id=1234-5678';
221
+
222
+ // Get as ArrayBuffer
223
+ const result = await downloadFigmaFrameImage(figmaUrl, {
224
+ apiKey: 'your-figma-api-key',
225
+ returnBuffer: true,
226
+ format: 'png'
227
+ });
228
+
229
+ console.log(`Buffer size: ${result.buffer.byteLength} bytes`);
230
+ console.log(`Dimensions: ${result.finalDimensions.width}x${result.finalDimensions.height}`);
231
+
232
+ // Use the buffer directly (e.g., upload to cloud storage, process with sharp, etc.)
233
+ // const processedImage = await sharp(Buffer.from(result.buffer)).resize(100, 100).toBuffer();
234
+ ```
235
+
199
236
  ### Download Multiple Frame Images
200
237
 
201
238
  ```typescript
@@ -216,6 +253,31 @@ const results = await downloadFigmaImages(
216
253
  );
217
254
  ```
218
255
 
256
+ ### Download Multiple Images as Buffers
257
+
258
+ ```typescript
259
+ import { downloadFigmaImages } from 'figma-metadata-extractor';
260
+
261
+ // Get multiple images as ArrayBuffers
262
+ const results = await downloadFigmaImages(
263
+ 'https://figma.com/file/ABC123/My-Design',
264
+ [
265
+ { nodeId: '1234:5678', fileName: 'frame1.png' },
266
+ { nodeId: '9876:5432', fileName: 'frame2.png' }
267
+ ],
268
+ {
269
+ apiKey: 'your-figma-api-key',
270
+ returnBuffer: true
271
+ }
272
+ );
273
+
274
+ // Process each buffer
275
+ results.forEach((result, index) => {
276
+ console.log(`Image ${index}: ${result.buffer.byteLength} bytes`);
277
+ // Upload to S3, process with sharp, etc.
278
+ });
279
+ ```
280
+
219
281
  ## Advanced Usage
220
282
 
221
283
  The library also exports the underlying extractor system for custom processing:
package/dist/index.cjs CHANGED
@@ -64,11 +64,69 @@ async function getImageDimensions(imagePath) {
64
64
  return { width: 1e3, height: 1e3 };
65
65
  }
66
66
  }
67
- async function downloadAndProcessImage(fileName, localPath, imageUrl, needsCropping = false, cropTransform, requiresImageDimensions = false) {
67
+ async function downloadAndProcessImage(fileName, localPath, imageUrl, needsCropping = false, cropTransform, requiresImageDimensions = false, returnBuffer = false) {
68
68
  const { Logger: Logger2 } = await Promise.resolve().then(() => logger);
69
69
  const processingLog = [];
70
70
  const { downloadFigmaImage: downloadFigmaImage2 } = await Promise.resolve().then(() => common);
71
- const originalPath = await downloadFigmaImage2(fileName, localPath, imageUrl);
71
+ const downloadResult = await downloadFigmaImage2(fileName, localPath, imageUrl, returnBuffer);
72
+ if (returnBuffer && downloadResult instanceof ArrayBuffer) {
73
+ Logger2.log(`Downloaded image as buffer (${downloadResult.byteLength} bytes)`);
74
+ let imageBuffer = Buffer.from(downloadResult);
75
+ let sharpImage = sharp(imageBuffer);
76
+ const metadata = await sharpImage.metadata();
77
+ const originalDimensions2 = {
78
+ width: metadata.width || 0,
79
+ height: metadata.height || 0
80
+ };
81
+ Logger2.log(`Original dimensions: ${originalDimensions2.width}x${originalDimensions2.height}`);
82
+ let wasCropped2 = false;
83
+ let cropRegion2;
84
+ let finalDimensions2 = originalDimensions2;
85
+ if (needsCropping && cropTransform) {
86
+ Logger2.log("Applying crop transform to buffer...");
87
+ const scaleX = cropTransform[0]?.[0] ?? 1;
88
+ const scaleY = cropTransform[1]?.[1] ?? 1;
89
+ const translateX = cropTransform[0]?.[2] ?? 0;
90
+ const translateY = cropTransform[1]?.[2] ?? 0;
91
+ const cropLeft = Math.max(0, Math.round(translateX * originalDimensions2.width));
92
+ const cropTop = Math.max(0, Math.round(translateY * originalDimensions2.height));
93
+ const cropWidth = Math.min(
94
+ originalDimensions2.width - cropLeft,
95
+ Math.round(scaleX * originalDimensions2.width)
96
+ );
97
+ const cropHeight = Math.min(
98
+ originalDimensions2.height - cropTop,
99
+ Math.round(scaleY * originalDimensions2.height)
100
+ );
101
+ if (cropWidth > 0 && cropHeight > 0) {
102
+ cropRegion2 = { left: cropLeft, top: cropTop, width: cropWidth, height: cropHeight };
103
+ const croppedBuffer = await sharpImage.extract({
104
+ left: cropLeft,
105
+ top: cropTop,
106
+ width: cropWidth,
107
+ height: cropHeight
108
+ }).toBuffer();
109
+ imageBuffer = croppedBuffer;
110
+ wasCropped2 = true;
111
+ finalDimensions2 = { width: cropWidth, height: cropHeight };
112
+ Logger2.log(`Cropped to region: ${cropLeft}, ${cropTop}, ${cropWidth}x${cropHeight}`);
113
+ }
114
+ }
115
+ let cssVariables2;
116
+ if (requiresImageDimensions) {
117
+ cssVariables2 = generateImageCSSVariables(finalDimensions2);
118
+ }
119
+ return {
120
+ buffer: imageBuffer.buffer.slice(imageBuffer.byteOffset, imageBuffer.byteOffset + imageBuffer.byteLength),
121
+ originalDimensions: originalDimensions2,
122
+ finalDimensions: finalDimensions2,
123
+ wasCropped: wasCropped2,
124
+ cropRegion: cropRegion2,
125
+ cssVariables: cssVariables2,
126
+ processingLog
127
+ };
128
+ }
129
+ const originalPath = downloadResult;
72
130
  Logger2.log(`Downloaded original image: ${originalPath}`);
73
131
  const originalDimensions = await getImageDimensions(originalPath);
74
132
  Logger2.log(`Original dimensions: ${originalDimensions.width}x${originalDimensions.height}`);
@@ -124,6 +182,7 @@ function generateImageCSSVariables({
124
182
  }
125
183
  const Logger = {
126
184
  isHTTP: false,
185
+ enableLogging: false,
127
186
  log: (...args) => {
128
187
  if (Logger.isHTTP) {
129
188
  console.log("[INFO]", ...args);
@@ -136,6 +195,7 @@ const Logger = {
136
195
  }
137
196
  };
138
197
  function writeLogs(name, value) {
198
+ if (!Logger.enableLogging) return;
139
199
  if (process.env.NODE_ENV !== "development") return;
140
200
  try {
141
201
  const logsDir = "logs";
@@ -300,17 +360,21 @@ class FigmaService {
300
360
  * - PNG vs SVG format (based on filename extension)
301
361
  * - Image cropping based on transform matrices
302
362
  * - CSS variable generation for image dimensions
363
+ * - Returning as ArrayBuffer instead of saving to disk
303
364
  *
304
365
  * @returns Array of local file paths for successfully downloaded images
305
366
  */
306
367
  async downloadImages(fileKey, localPath, items, options = {}) {
307
368
  if (items.length === 0) return [];
308
- const sanitizedPath = path.normalize(localPath).replace(/^(\.\.(\/|\\|$))+/, "");
309
- const resolvedPath = path.resolve(sanitizedPath);
310
- if (!resolvedPath.startsWith(path.resolve(process.cwd()))) {
311
- throw new Error("Invalid path specified. Directory traversal is not allowed.");
369
+ const { pngScale = 2, svgOptions, returnBuffer = false } = options;
370
+ let resolvedPath = "";
371
+ if (!returnBuffer) {
372
+ const sanitizedPath = path.normalize(localPath).replace(/^(\.\.(\/|\\|$))+/, "");
373
+ resolvedPath = path.resolve(sanitizedPath);
374
+ if (!resolvedPath.startsWith(path.resolve(process.cwd()))) {
375
+ throw new Error("Invalid path specified. Directory traversal is not allowed.");
376
+ }
312
377
  }
313
- const { pngScale = 2, svgOptions } = options;
314
378
  const downloadPromises = [];
315
379
  const imageFills = items.filter(
316
380
  (item) => !!item.imageRef
@@ -328,7 +392,8 @@ class FigmaService {
328
392
  imageUrl,
329
393
  needsCropping,
330
394
  cropTransform,
331
- requiresImageDimensions
395
+ requiresImageDimensions,
396
+ returnBuffer
332
397
  ) : null;
333
398
  }).filter((promise) => promise !== null);
334
399
  if (fillDownloads.length > 0) {
@@ -353,7 +418,8 @@ class FigmaService {
353
418
  imageUrl,
354
419
  needsCropping,
355
420
  cropTransform,
356
- requiresImageDimensions
421
+ requiresImageDimensions,
422
+ returnBuffer
357
423
  ) : null;
358
424
  }).filter((promise) => promise !== null);
359
425
  if (pngDownloads.length > 0) {
@@ -375,7 +441,8 @@ class FigmaService {
375
441
  imageUrl,
376
442
  needsCropping,
377
443
  cropTransform,
378
- requiresImageDimensions
444
+ requiresImageDimensions,
445
+ returnBuffer
379
446
  ) : null;
380
447
  }).filter((promise) => promise !== null);
381
448
  if (svgDownloads.length > 0) {
@@ -409,23 +476,27 @@ class FigmaService {
409
476
  return response;
410
477
  }
411
478
  }
412
- async function downloadFigmaImage(fileName, localPath, imageUrl) {
479
+ async function downloadFigmaImage(fileName, localPath, imageUrl, returnBuffer = false) {
413
480
  try {
414
- if (!fs.existsSync(localPath)) {
415
- fs.mkdirSync(localPath, { recursive: true });
416
- }
417
- const fullPath = path.join(localPath, fileName);
418
481
  const response = await fetch(imageUrl, {
419
482
  method: "GET"
420
483
  });
421
484
  if (!response.ok) {
422
485
  throw new Error(`Failed to download image: ${response.statusText}`);
423
486
  }
424
- const writer = fs.createWriteStream(fullPath);
487
+ if (returnBuffer) {
488
+ const arrayBuffer = await response.arrayBuffer();
489
+ return arrayBuffer;
490
+ }
491
+ if (!fs.existsSync(localPath)) {
492
+ fs.mkdirSync(localPath, { recursive: true });
493
+ }
494
+ const fullPath = path.join(localPath, fileName);
425
495
  const reader = response.body?.getReader();
426
496
  if (!reader) {
427
497
  throw new Error("Failed to get response body");
428
498
  }
499
+ const writer = fs.createWriteStream(fullPath);
429
500
  return new Promise((resolve, reject) => {
430
501
  const processStream = async () => {
431
502
  try {
@@ -1375,8 +1446,10 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1375
1446
  localPath,
1376
1447
  imageFormat = "png",
1377
1448
  pngScale = 2,
1378
- useRelativePaths = true
1449
+ useRelativePaths = true,
1450
+ enableLogging = false
1379
1451
  } = options;
1452
+ Logger.enableLogging = enableLogging;
1380
1453
  if (!apiKey && !oauthToken) {
1381
1454
  throw new Error("Either apiKey or oauthToken is required");
1382
1455
  }
@@ -1451,10 +1524,14 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1451
1524
  }
1452
1525
  }
1453
1526
  async function downloadFigmaImages(figmaUrl, nodes, options) {
1454
- const { apiKey, oauthToken, useOAuth = false, pngScale = 2, localPath } = options;
1527
+ const { apiKey, oauthToken, useOAuth = false, pngScale = 2, localPath, enableLogging = false, returnBuffer = false } = options;
1528
+ Logger.enableLogging = enableLogging;
1455
1529
  if (!apiKey && !oauthToken) {
1456
1530
  throw new Error("Either apiKey or oauthToken is required");
1457
1531
  }
1532
+ if (!returnBuffer && !localPath) {
1533
+ throw new Error("localPath is required when returnBuffer is false");
1534
+ }
1458
1535
  const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
1459
1536
  if (!urlMatch) {
1460
1537
  throw new Error("Invalid Figma URL format");
@@ -1470,8 +1547,9 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
1470
1547
  ...node,
1471
1548
  nodeId: node.nodeId.replace(/-/g, ":")
1472
1549
  }));
1473
- const results = await figmaService.downloadImages(fileKey, localPath, processedNodes, {
1474
- pngScale
1550
+ const results = await figmaService.downloadImages(fileKey, localPath || "", processedNodes, {
1551
+ pngScale,
1552
+ returnBuffer
1475
1553
  });
1476
1554
  return results;
1477
1555
  } catch (error) {
@@ -1487,11 +1565,17 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1487
1565
  pngScale = 2,
1488
1566
  localPath,
1489
1567
  fileName,
1490
- format = "png"
1568
+ format = "png",
1569
+ enableLogging = false,
1570
+ returnBuffer = false
1491
1571
  } = options;
1572
+ Logger.enableLogging = enableLogging;
1492
1573
  if (!apiKey && !oauthToken) {
1493
1574
  throw new Error("Either apiKey or oauthToken is required");
1494
1575
  }
1576
+ if (!returnBuffer && (!localPath || !fileName)) {
1577
+ throw new Error("localPath and fileName are required when returnBuffer is false");
1578
+ }
1495
1579
  const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
1496
1580
  if (!urlMatch) {
1497
1581
  throw new Error("Invalid Figma URL format");
@@ -1502,9 +1586,11 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1502
1586
  throw new Error("No frame node-id found in URL. Please provide a Figma URL with a node-id parameter (e.g., ?node-id=123-456)");
1503
1587
  }
1504
1588
  const nodeId = nodeIdMatch[1].replace(/-/g, ":");
1505
- const expectedExtension = `.${format}`;
1506
- if (!fileName.toLowerCase().endsWith(expectedExtension)) {
1507
- throw new Error(`Filename must end with ${expectedExtension} for ${format} format`);
1589
+ if (fileName) {
1590
+ const expectedExtension = `.${format}`;
1591
+ if (!fileName.toLowerCase().endsWith(expectedExtension)) {
1592
+ throw new Error(`Filename must end with ${expectedExtension} for ${format} format`);
1593
+ }
1508
1594
  }
1509
1595
  const figmaService = new FigmaService({
1510
1596
  figmaApiKey: apiKey || "",
@@ -1515,15 +1601,20 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1515
1601
  Logger.log(`Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`);
1516
1602
  const imageNode = {
1517
1603
  nodeId,
1518
- fileName
1604
+ fileName: fileName || `temp.${format}`
1519
1605
  };
1520
- const results = await figmaService.downloadImages(fileKey, localPath, [imageNode], {
1521
- pngScale: format === "png" ? pngScale : void 0
1606
+ const results = await figmaService.downloadImages(fileKey, localPath || "", [imageNode], {
1607
+ pngScale: format === "png" ? pngScale : void 0,
1608
+ returnBuffer
1522
1609
  });
1523
1610
  if (results.length === 0) {
1524
1611
  throw new Error(`Failed to download image for frame ${nodeId}`);
1525
1612
  }
1526
- Logger.log(`Successfully downloaded frame image to: ${results[0].filePath}`);
1613
+ if (returnBuffer) {
1614
+ Logger.log(`Successfully downloaded frame image as buffer`);
1615
+ } else {
1616
+ Logger.log(`Successfully downloaded frame image to: ${results[0].filePath}`);
1617
+ }
1527
1618
  return results[0];
1528
1619
  } catch (error) {
1529
1620
  Logger.error(`Error downloading frame image from ${fileKey}:`, error);
package/dist/index.js CHANGED
@@ -62,11 +62,69 @@ async function getImageDimensions(imagePath) {
62
62
  return { width: 1e3, height: 1e3 };
63
63
  }
64
64
  }
65
- async function downloadAndProcessImage(fileName, localPath, imageUrl, needsCropping = false, cropTransform, requiresImageDimensions = false) {
65
+ async function downloadAndProcessImage(fileName, localPath, imageUrl, needsCropping = false, cropTransform, requiresImageDimensions = false, returnBuffer = false) {
66
66
  const { Logger: Logger2 } = await Promise.resolve().then(() => logger);
67
67
  const processingLog = [];
68
68
  const { downloadFigmaImage: downloadFigmaImage2 } = await Promise.resolve().then(() => common);
69
- const originalPath = await downloadFigmaImage2(fileName, localPath, imageUrl);
69
+ const downloadResult = await downloadFigmaImage2(fileName, localPath, imageUrl, returnBuffer);
70
+ if (returnBuffer && downloadResult instanceof ArrayBuffer) {
71
+ Logger2.log(`Downloaded image as buffer (${downloadResult.byteLength} bytes)`);
72
+ let imageBuffer = Buffer.from(downloadResult);
73
+ let sharpImage = sharp(imageBuffer);
74
+ const metadata = await sharpImage.metadata();
75
+ const originalDimensions2 = {
76
+ width: metadata.width || 0,
77
+ height: metadata.height || 0
78
+ };
79
+ Logger2.log(`Original dimensions: ${originalDimensions2.width}x${originalDimensions2.height}`);
80
+ let wasCropped2 = false;
81
+ let cropRegion2;
82
+ let finalDimensions2 = originalDimensions2;
83
+ if (needsCropping && cropTransform) {
84
+ Logger2.log("Applying crop transform to buffer...");
85
+ const scaleX = cropTransform[0]?.[0] ?? 1;
86
+ const scaleY = cropTransform[1]?.[1] ?? 1;
87
+ const translateX = cropTransform[0]?.[2] ?? 0;
88
+ const translateY = cropTransform[1]?.[2] ?? 0;
89
+ const cropLeft = Math.max(0, Math.round(translateX * originalDimensions2.width));
90
+ const cropTop = Math.max(0, Math.round(translateY * originalDimensions2.height));
91
+ const cropWidth = Math.min(
92
+ originalDimensions2.width - cropLeft,
93
+ Math.round(scaleX * originalDimensions2.width)
94
+ );
95
+ const cropHeight = Math.min(
96
+ originalDimensions2.height - cropTop,
97
+ Math.round(scaleY * originalDimensions2.height)
98
+ );
99
+ if (cropWidth > 0 && cropHeight > 0) {
100
+ cropRegion2 = { left: cropLeft, top: cropTop, width: cropWidth, height: cropHeight };
101
+ const croppedBuffer = await sharpImage.extract({
102
+ left: cropLeft,
103
+ top: cropTop,
104
+ width: cropWidth,
105
+ height: cropHeight
106
+ }).toBuffer();
107
+ imageBuffer = croppedBuffer;
108
+ wasCropped2 = true;
109
+ finalDimensions2 = { width: cropWidth, height: cropHeight };
110
+ Logger2.log(`Cropped to region: ${cropLeft}, ${cropTop}, ${cropWidth}x${cropHeight}`);
111
+ }
112
+ }
113
+ let cssVariables2;
114
+ if (requiresImageDimensions) {
115
+ cssVariables2 = generateImageCSSVariables(finalDimensions2);
116
+ }
117
+ return {
118
+ buffer: imageBuffer.buffer.slice(imageBuffer.byteOffset, imageBuffer.byteOffset + imageBuffer.byteLength),
119
+ originalDimensions: originalDimensions2,
120
+ finalDimensions: finalDimensions2,
121
+ wasCropped: wasCropped2,
122
+ cropRegion: cropRegion2,
123
+ cssVariables: cssVariables2,
124
+ processingLog
125
+ };
126
+ }
127
+ const originalPath = downloadResult;
70
128
  Logger2.log(`Downloaded original image: ${originalPath}`);
71
129
  const originalDimensions = await getImageDimensions(originalPath);
72
130
  Logger2.log(`Original dimensions: ${originalDimensions.width}x${originalDimensions.height}`);
@@ -122,6 +180,7 @@ function generateImageCSSVariables({
122
180
  }
123
181
  const Logger = {
124
182
  isHTTP: false,
183
+ enableLogging: false,
125
184
  log: (...args) => {
126
185
  if (Logger.isHTTP) {
127
186
  console.log("[INFO]", ...args);
@@ -134,6 +193,7 @@ const Logger = {
134
193
  }
135
194
  };
136
195
  function writeLogs(name, value) {
196
+ if (!Logger.enableLogging) return;
137
197
  if (process.env.NODE_ENV !== "development") return;
138
198
  try {
139
199
  const logsDir = "logs";
@@ -298,17 +358,21 @@ class FigmaService {
298
358
  * - PNG vs SVG format (based on filename extension)
299
359
  * - Image cropping based on transform matrices
300
360
  * - CSS variable generation for image dimensions
361
+ * - Returning as ArrayBuffer instead of saving to disk
301
362
  *
302
363
  * @returns Array of local file paths for successfully downloaded images
303
364
  */
304
365
  async downloadImages(fileKey, localPath, items, options = {}) {
305
366
  if (items.length === 0) return [];
306
- const sanitizedPath = path.normalize(localPath).replace(/^(\.\.(\/|\\|$))+/, "");
307
- const resolvedPath = path.resolve(sanitizedPath);
308
- if (!resolvedPath.startsWith(path.resolve(process.cwd()))) {
309
- throw new Error("Invalid path specified. Directory traversal is not allowed.");
367
+ const { pngScale = 2, svgOptions, returnBuffer = false } = options;
368
+ let resolvedPath = "";
369
+ if (!returnBuffer) {
370
+ const sanitizedPath = path.normalize(localPath).replace(/^(\.\.(\/|\\|$))+/, "");
371
+ resolvedPath = path.resolve(sanitizedPath);
372
+ if (!resolvedPath.startsWith(path.resolve(process.cwd()))) {
373
+ throw new Error("Invalid path specified. Directory traversal is not allowed.");
374
+ }
310
375
  }
311
- const { pngScale = 2, svgOptions } = options;
312
376
  const downloadPromises = [];
313
377
  const imageFills = items.filter(
314
378
  (item) => !!item.imageRef
@@ -326,7 +390,8 @@ class FigmaService {
326
390
  imageUrl,
327
391
  needsCropping,
328
392
  cropTransform,
329
- requiresImageDimensions
393
+ requiresImageDimensions,
394
+ returnBuffer
330
395
  ) : null;
331
396
  }).filter((promise) => promise !== null);
332
397
  if (fillDownloads.length > 0) {
@@ -351,7 +416,8 @@ class FigmaService {
351
416
  imageUrl,
352
417
  needsCropping,
353
418
  cropTransform,
354
- requiresImageDimensions
419
+ requiresImageDimensions,
420
+ returnBuffer
355
421
  ) : null;
356
422
  }).filter((promise) => promise !== null);
357
423
  if (pngDownloads.length > 0) {
@@ -373,7 +439,8 @@ class FigmaService {
373
439
  imageUrl,
374
440
  needsCropping,
375
441
  cropTransform,
376
- requiresImageDimensions
442
+ requiresImageDimensions,
443
+ returnBuffer
377
444
  ) : null;
378
445
  }).filter((promise) => promise !== null);
379
446
  if (svgDownloads.length > 0) {
@@ -407,23 +474,27 @@ class FigmaService {
407
474
  return response;
408
475
  }
409
476
  }
410
- async function downloadFigmaImage(fileName, localPath, imageUrl) {
477
+ async function downloadFigmaImage(fileName, localPath, imageUrl, returnBuffer = false) {
411
478
  try {
412
- if (!fs.existsSync(localPath)) {
413
- fs.mkdirSync(localPath, { recursive: true });
414
- }
415
- const fullPath = path.join(localPath, fileName);
416
479
  const response = await fetch(imageUrl, {
417
480
  method: "GET"
418
481
  });
419
482
  if (!response.ok) {
420
483
  throw new Error(`Failed to download image: ${response.statusText}`);
421
484
  }
422
- const writer = fs.createWriteStream(fullPath);
485
+ if (returnBuffer) {
486
+ const arrayBuffer = await response.arrayBuffer();
487
+ return arrayBuffer;
488
+ }
489
+ if (!fs.existsSync(localPath)) {
490
+ fs.mkdirSync(localPath, { recursive: true });
491
+ }
492
+ const fullPath = path.join(localPath, fileName);
423
493
  const reader = response.body?.getReader();
424
494
  if (!reader) {
425
495
  throw new Error("Failed to get response body");
426
496
  }
497
+ const writer = fs.createWriteStream(fullPath);
427
498
  return new Promise((resolve, reject) => {
428
499
  const processStream = async () => {
429
500
  try {
@@ -1373,8 +1444,10 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1373
1444
  localPath,
1374
1445
  imageFormat = "png",
1375
1446
  pngScale = 2,
1376
- useRelativePaths = true
1447
+ useRelativePaths = true,
1448
+ enableLogging = false
1377
1449
  } = options;
1450
+ Logger.enableLogging = enableLogging;
1378
1451
  if (!apiKey && !oauthToken) {
1379
1452
  throw new Error("Either apiKey or oauthToken is required");
1380
1453
  }
@@ -1449,10 +1522,14 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1449
1522
  }
1450
1523
  }
1451
1524
  async function downloadFigmaImages(figmaUrl, nodes, options) {
1452
- const { apiKey, oauthToken, useOAuth = false, pngScale = 2, localPath } = options;
1525
+ const { apiKey, oauthToken, useOAuth = false, pngScale = 2, localPath, enableLogging = false, returnBuffer = false } = options;
1526
+ Logger.enableLogging = enableLogging;
1453
1527
  if (!apiKey && !oauthToken) {
1454
1528
  throw new Error("Either apiKey or oauthToken is required");
1455
1529
  }
1530
+ if (!returnBuffer && !localPath) {
1531
+ throw new Error("localPath is required when returnBuffer is false");
1532
+ }
1456
1533
  const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
1457
1534
  if (!urlMatch) {
1458
1535
  throw new Error("Invalid Figma URL format");
@@ -1468,8 +1545,9 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
1468
1545
  ...node,
1469
1546
  nodeId: node.nodeId.replace(/-/g, ":")
1470
1547
  }));
1471
- const results = await figmaService.downloadImages(fileKey, localPath, processedNodes, {
1472
- pngScale
1548
+ const results = await figmaService.downloadImages(fileKey, localPath || "", processedNodes, {
1549
+ pngScale,
1550
+ returnBuffer
1473
1551
  });
1474
1552
  return results;
1475
1553
  } catch (error) {
@@ -1485,11 +1563,17 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1485
1563
  pngScale = 2,
1486
1564
  localPath,
1487
1565
  fileName,
1488
- format = "png"
1566
+ format = "png",
1567
+ enableLogging = false,
1568
+ returnBuffer = false
1489
1569
  } = options;
1570
+ Logger.enableLogging = enableLogging;
1490
1571
  if (!apiKey && !oauthToken) {
1491
1572
  throw new Error("Either apiKey or oauthToken is required");
1492
1573
  }
1574
+ if (!returnBuffer && (!localPath || !fileName)) {
1575
+ throw new Error("localPath and fileName are required when returnBuffer is false");
1576
+ }
1493
1577
  const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
1494
1578
  if (!urlMatch) {
1495
1579
  throw new Error("Invalid Figma URL format");
@@ -1500,9 +1584,11 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1500
1584
  throw new Error("No frame node-id found in URL. Please provide a Figma URL with a node-id parameter (e.g., ?node-id=123-456)");
1501
1585
  }
1502
1586
  const nodeId = nodeIdMatch[1].replace(/-/g, ":");
1503
- const expectedExtension = `.${format}`;
1504
- if (!fileName.toLowerCase().endsWith(expectedExtension)) {
1505
- throw new Error(`Filename must end with ${expectedExtension} for ${format} format`);
1587
+ if (fileName) {
1588
+ const expectedExtension = `.${format}`;
1589
+ if (!fileName.toLowerCase().endsWith(expectedExtension)) {
1590
+ throw new Error(`Filename must end with ${expectedExtension} for ${format} format`);
1591
+ }
1506
1592
  }
1507
1593
  const figmaService = new FigmaService({
1508
1594
  figmaApiKey: apiKey || "",
@@ -1513,15 +1599,20 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
1513
1599
  Logger.log(`Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`);
1514
1600
  const imageNode = {
1515
1601
  nodeId,
1516
- fileName
1602
+ fileName: fileName || `temp.${format}`
1517
1603
  };
1518
- const results = await figmaService.downloadImages(fileKey, localPath, [imageNode], {
1519
- pngScale: format === "png" ? pngScale : void 0
1604
+ const results = await figmaService.downloadImages(fileKey, localPath || "", [imageNode], {
1605
+ pngScale: format === "png" ? pngScale : void 0,
1606
+ returnBuffer
1520
1607
  });
1521
1608
  if (results.length === 0) {
1522
1609
  throw new Error(`Failed to download image for frame ${nodeId}`);
1523
1610
  }
1524
- Logger.log(`Successfully downloaded frame image to: ${results[0].filePath}`);
1611
+ if (returnBuffer) {
1612
+ Logger.log(`Successfully downloaded frame image as buffer`);
1613
+ } else {
1614
+ Logger.log(`Successfully downloaded frame image to: ${results[0].filePath}`);
1615
+ }
1525
1616
  return results[0];
1526
1617
  } catch (error) {
1527
1618
  Logger.error(`Error downloading frame image from ${fileKey}:`, error);
package/dist/lib.d.ts CHANGED
@@ -25,12 +25,16 @@ export interface FigmaMetadataOptions {
25
25
  * Default: true
26
26
  */
27
27
  useRelativePaths?: boolean | string;
28
+ /** Enable JSON debug log files (defaults to false) */
29
+ enableLogging?: boolean;
28
30
  }
29
31
  export interface FigmaImageOptions {
30
32
  /** Export scale for PNG images (defaults to 2) */
31
33
  pngScale?: number;
32
- /** The absolute path to the directory where images should be stored */
33
- localPath: string;
34
+ /** The absolute path to the directory where images should be stored (optional if returnBuffer is true) */
35
+ localPath?: string;
36
+ /** Return images as ArrayBuffer instead of saving to disk (defaults to false) */
37
+ returnBuffer?: boolean;
34
38
  }
35
39
  export interface FigmaImageNode {
36
40
  /** The ID of the Figma node, formatted as '1234:5678' */
@@ -54,7 +58,8 @@ export interface FigmaMetadataResult {
54
58
  globalVars: any;
55
59
  }
56
60
  export interface FigmaImageResult {
57
- filePath: string;
61
+ filePath?: string;
62
+ buffer?: ArrayBuffer;
58
63
  finalDimensions: {
59
64
  width: number;
60
65
  height: number;
@@ -71,12 +76,16 @@ export interface FigmaFrameImageOptions {
71
76
  useOAuth?: boolean;
72
77
  /** Export scale for PNG images (defaults to 2) */
73
78
  pngScale?: number;
74
- /** The absolute path to the directory where the image should be stored */
75
- localPath: string;
76
- /** The filename for the downloaded image (must end with .png or .svg) */
77
- fileName: string;
79
+ /** The absolute path to the directory where the image should be stored (optional if returnBuffer is true) */
80
+ localPath?: string;
81
+ /** The filename for the downloaded image (must end with .png or .svg, optional if returnBuffer is true) */
82
+ fileName?: string;
78
83
  /** Image format to download (defaults to 'png') */
79
84
  format?: 'png' | 'svg';
85
+ /** Enable JSON debug log files (defaults to false) */
86
+ enableLogging?: boolean;
87
+ /** Return image as ArrayBuffer instead of saving to disk (defaults to false) */
88
+ returnBuffer?: boolean;
80
89
  }
81
90
  /**
82
91
  * Extract metadata from a Figma file or specific nodes
@@ -49,6 +49,7 @@ export declare class FigmaService {
49
49
  * - PNG vs SVG format (based on filename extension)
50
50
  * - Image cropping based on transform matrices
51
51
  * - CSS variable generation for image dimensions
52
+ * - Returning as ArrayBuffer instead of saving to disk
52
53
  *
53
54
  * @returns Array of local file paths for successfully downloaded images
54
55
  */
@@ -62,6 +63,7 @@ export declare class FigmaService {
62
63
  }>, options?: {
63
64
  pngScale?: number;
64
65
  svgOptions?: SvgOptions;
66
+ returnBuffer?: boolean;
65
67
  }): Promise<ImageProcessingResult[]>;
66
68
  /**
67
69
  * Get raw Figma API response for a file (for use with flexible extractors)
@@ -2,14 +2,15 @@ export type StyleId = `${string}_${string}` & {
2
2
  __brand: "StyleId";
3
3
  };
4
4
  /**
5
- * Download Figma image and save it locally
5
+ * Download Figma image and save it locally or return as buffer
6
6
  * @param fileName - The filename to save as
7
7
  * @param localPath - The local path to save to
8
8
  * @param imageUrl - Image URL (images[nodeId])
9
- * @returns A Promise that resolves to the full file path where the image was saved
9
+ * @param returnBuffer - If true, return ArrayBuffer instead of saving to disk
10
+ * @returns A Promise that resolves to the full file path where the image was saved, or ArrayBuffer if returnBuffer is true
10
11
  * @throws Error if download fails
11
12
  */
12
- export declare function downloadFigmaImage(fileName: string, localPath: string, imageUrl: string): Promise<string>;
13
+ export declare function downloadFigmaImage(fileName: string, localPath: string, imageUrl: string, returnBuffer?: boolean): Promise<string | ArrayBuffer>;
13
14
  /**
14
15
  * Remove keys with empty arrays or empty objects from an object.
15
16
  * @param input - The input object or value.
@@ -16,7 +16,8 @@ export declare function getImageDimensions(imagePath: string): Promise<{
16
16
  height: number;
17
17
  }>;
18
18
  export type ImageProcessingResult = {
19
- filePath: string;
19
+ filePath?: string;
20
+ buffer?: ArrayBuffer;
20
21
  originalDimensions: {
21
22
  width: number;
22
23
  height: number;
@@ -43,9 +44,10 @@ export type ImageProcessingResult = {
43
44
  * @param needsCropping - Whether to apply crop transform
44
45
  * @param cropTransform - Transform matrix for cropping
45
46
  * @param requiresImageDimensions - Whether to generate dimension metadata
47
+ * @param returnBuffer - If true, return ArrayBuffer instead of saving to disk
46
48
  * @returns Promise<ImageProcessingResult> - Detailed processing information
47
49
  */
48
- export declare function downloadAndProcessImage(fileName: string, localPath: string, imageUrl: string, needsCropping?: boolean, cropTransform?: Transform, requiresImageDimensions?: boolean): Promise<ImageProcessingResult>;
50
+ export declare function downloadAndProcessImage(fileName: string, localPath: string, imageUrl: string, needsCropping?: boolean, cropTransform?: Transform, requiresImageDimensions?: boolean, returnBuffer?: boolean): Promise<ImageProcessingResult>;
49
51
  /**
50
52
  * Create CSS custom properties for image dimensions
51
53
  * @param imagePath - Path to the image file
@@ -1,5 +1,6 @@
1
1
  export declare const Logger: {
2
2
  isHTTP: boolean;
3
+ enableLogging: boolean;
3
4
  log: (...args: any[]) => void;
4
5
  error: (...args: any[]) => void;
5
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-metadata-extractor",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Extract metadata and download images from Figma files. A standalone library for accessing Figma design data and downloading frame images programmatically.",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",