figma-metadata-extractor 1.0.6 → 1.0.8
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 +176 -6
- package/dist/index.cjs +128 -35
- package/dist/index.js +128 -35
- package/dist/lib.d.ts +29 -7
- package/dist/services/figma.d.ts +2 -0
- package/dist/utils/common.d.ts +4 -3
- package/dist/utils/image-processing.d.ts +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,15 @@ A TypeScript library for extracting metadata and downloading images from Figma f
|
|
|
8
8
|
npm install figma-metadata-extractor
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- 🎨 Extract comprehensive metadata from Figma files
|
|
14
|
+
- 📦 Auto-download image assets (to disk or as buffers)
|
|
15
|
+
- 🔄 Support for both file-based and buffer-based workflows
|
|
16
|
+
- 🎯 Enrich metadata with image paths and markup
|
|
17
|
+
- 🔐 Support for both API keys and OAuth tokens
|
|
18
|
+
- 📝 Multiple output formats (JSON, YAML, object)
|
|
19
|
+
|
|
11
20
|
## Quick Start
|
|
12
21
|
|
|
13
22
|
### Get Metadata with Auto-Downloaded Images (LLM-Ready!)
|
|
@@ -15,7 +24,7 @@ npm install figma-metadata-extractor
|
|
|
15
24
|
```typescript
|
|
16
25
|
import { getFigmaMetadata } from 'figma-metadata-extractor';
|
|
17
26
|
|
|
18
|
-
// Extract metadata AND automatically download image assets
|
|
27
|
+
// Extract metadata AND automatically download image assets to disk
|
|
19
28
|
const metadata = await getFigmaMetadata(
|
|
20
29
|
'https://figma.com/file/ABC123/My-Design',
|
|
21
30
|
{
|
|
@@ -26,6 +35,55 @@ const metadata = await getFigmaMetadata(
|
|
|
26
35
|
}
|
|
27
36
|
);
|
|
28
37
|
|
|
38
|
+
// Nodes are enriched with downloadedImage property
|
|
39
|
+
metadata.nodes.forEach(node => {
|
|
40
|
+
if (node.downloadedImage) {
|
|
41
|
+
console.log(node.downloadedImage.filePath);
|
|
42
|
+
console.log(node.downloadedImage.markdown); // 
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Get Images as Buffers (No Disk Write)
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { getFigmaMetadata, enrichMetadataWithImages } from 'figma-metadata-extractor';
|
|
51
|
+
import fs from 'fs/promises';
|
|
52
|
+
|
|
53
|
+
// Get metadata with images as ArrayBuffers
|
|
54
|
+
const result = await getFigmaMetadata(
|
|
55
|
+
'https://figma.com/file/ABC123/My-Design',
|
|
56
|
+
{
|
|
57
|
+
apiKey: 'your-figma-api-key',
|
|
58
|
+
downloadImages: true,
|
|
59
|
+
returnBuffer: true // Get images as ArrayBuffer
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Images are returned separately, metadata is not enriched
|
|
64
|
+
console.log(`Downloaded ${result.images.length} images as buffers`);
|
|
65
|
+
|
|
66
|
+
// Process buffers (upload to S3, convert format, etc.)
|
|
67
|
+
const savedPaths: string[] = [];
|
|
68
|
+
for (const image of result.images) {
|
|
69
|
+
// Example: Save to disk after processing
|
|
70
|
+
const buffer = Buffer.from(image.buffer);
|
|
71
|
+
const path = `./processed/${Date.now()}.png`;
|
|
72
|
+
await fs.writeFile(path, buffer);
|
|
73
|
+
savedPaths.push(path);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Optionally enrich metadata with saved file paths
|
|
77
|
+
const enrichedMetadata = enrichMetadataWithImages(result, savedPaths, {
|
|
78
|
+
useRelativePaths: true
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Now nodes have downloadedImage properties
|
|
82
|
+
enrichedMetadata.nodes.forEach(node => {
|
|
83
|
+
if (node.downloadedImage) {
|
|
84
|
+
console.log(node.downloadedImage.markdown);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
29
87
|
```
|
|
30
88
|
|
|
31
89
|
### Get Metadata Only (No Downloads)
|
|
@@ -98,13 +156,25 @@ Extracts comprehensive metadata from a Figma file including layout, content, vis
|
|
|
98
156
|
- `outputFormat?: 'json' | 'yaml' | 'object'` - Output format (default: 'object')
|
|
99
157
|
- `depth?: number` - Maximum depth to traverse the node tree
|
|
100
158
|
- `downloadImages?: boolean` - Automatically download image assets and enrich metadata (default: false)
|
|
101
|
-
- `localPath?: string` - Local path for downloaded images (
|
|
159
|
+
- `localPath?: string` - Local path for downloaded images (optional if returnBuffer is true)
|
|
102
160
|
- `imageFormat?: 'png' | 'svg'` - Image format for downloads (default: 'png')
|
|
103
161
|
- `pngScale?: number` - Export scale for PNG images (default: 2)
|
|
162
|
+
- `returnBuffer?: boolean` - Return images as ArrayBuffer instead of saving to disk (default: false)
|
|
163
|
+
- `enableLogging?: boolean` - Enable JSON debug log files (default: false)
|
|
104
164
|
|
|
105
165
|
**Returns:** Promise<FigmaMetadataResult | string>
|
|
106
166
|
|
|
107
|
-
|
|
167
|
+
**FigmaMetadataResult:**
|
|
168
|
+
```typescript
|
|
169
|
+
{
|
|
170
|
+
metadata: any; // File metadata
|
|
171
|
+
nodes: any[]; // Design nodes
|
|
172
|
+
globalVars: any; // Styles, colors, etc.
|
|
173
|
+
images?: FigmaImageResult[]; // Only present when downloadImages: true and returnBuffer: true
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
When `downloadImages: true` and `returnBuffer: false`, nodes with image assets will include a `downloadedImage` property:
|
|
108
178
|
```typescript
|
|
109
179
|
{
|
|
110
180
|
filePath: string; // Absolute path
|
|
@@ -115,6 +185,8 @@ When `downloadImages` is true, nodes with image assets will include a `downloade
|
|
|
115
185
|
}
|
|
116
186
|
```
|
|
117
187
|
|
|
188
|
+
When `downloadImages: true` and `returnBuffer: true`, images are returned in the `images` array and nodes are NOT enriched. Use `enrichMetadataWithImages()` to enrich them later after saving buffers to disk.
|
|
189
|
+
|
|
118
190
|
### `downloadFigmaImages(figmaUrl, nodes, options)`
|
|
119
191
|
|
|
120
192
|
Downloads SVG and PNG images from a Figma file.
|
|
@@ -135,10 +207,50 @@ Downloads SVG and PNG images from a Figma file.
|
|
|
135
207
|
|
|
136
208
|
**Additional Options:**
|
|
137
209
|
- `pngScale?: number` - Export scale for PNG images (default: 2)
|
|
138
|
-
- `localPath
|
|
210
|
+
- `localPath?: string` - Absolute path to save images (optional if returnBuffer is true)
|
|
211
|
+
- `returnBuffer?: boolean` - Return images as ArrayBuffer instead of saving to disk (default: false)
|
|
212
|
+
- `enableLogging?: boolean` - Enable JSON debug log files (default: false)
|
|
139
213
|
|
|
140
214
|
**Returns:** Promise<FigmaImageResult[]>
|
|
141
215
|
|
|
216
|
+
When `returnBuffer` is true, each result will contain a `buffer` property instead of `filePath`.
|
|
217
|
+
|
|
218
|
+
### `enrichMetadataWithImages(metadata, imagePaths, options)`
|
|
219
|
+
|
|
220
|
+
Enriches metadata with saved image file paths after saving buffers to disk.
|
|
221
|
+
|
|
222
|
+
**Parameters:**
|
|
223
|
+
- `metadata` (FigmaMetadataResult): The metadata result from getFigmaMetadata with returnBuffer: true
|
|
224
|
+
- `imagePaths` (string[]): Array of file paths where images were saved (must match order of metadata.images)
|
|
225
|
+
- `options` (object): Configuration options
|
|
226
|
+
- `useRelativePaths?: boolean | string` - How to generate paths (default: true)
|
|
227
|
+
- `localPath?: string` - Base path for relative path calculation
|
|
228
|
+
|
|
229
|
+
**Returns:** FigmaMetadataResult with enriched nodes
|
|
230
|
+
|
|
231
|
+
**Example:**
|
|
232
|
+
```typescript
|
|
233
|
+
// Get metadata with buffers
|
|
234
|
+
const result = await getFigmaMetadata(url, {
|
|
235
|
+
apiKey: 'key',
|
|
236
|
+
downloadImages: true,
|
|
237
|
+
returnBuffer: true
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Save buffers to disk
|
|
241
|
+
const paths = await Promise.all(
|
|
242
|
+
result.images.map((img, i) =>
|
|
243
|
+
fs.writeFile(`./images/img-${i}.png`, Buffer.from(img.buffer))
|
|
244
|
+
.then(() => `./images/img-${i}.png`)
|
|
245
|
+
)
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Enrich metadata with file paths
|
|
249
|
+
const enriched = enrichMetadataWithImages(result, paths, {
|
|
250
|
+
useRelativePaths: true
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
142
254
|
### `downloadFigmaFrameImage(figmaUrl, options)`
|
|
143
255
|
|
|
144
256
|
Downloads a single frame image from a Figma URL that contains a node-id parameter.
|
|
@@ -151,13 +263,22 @@ Downloads a single frame image from a Figma URL that contains a node-id paramete
|
|
|
151
263
|
- `apiKey?: string` - Figma API key (Personal Access Token)
|
|
152
264
|
- `oauthToken?: string` - Figma OAuth Bearer token
|
|
153
265
|
- `useOAuth?: boolean` - Whether to use OAuth instead of API key
|
|
154
|
-
- `localPath
|
|
155
|
-
- `fileName
|
|
266
|
+
- `localPath?: string` - Absolute path to save the image (optional if returnBuffer is true)
|
|
267
|
+
- `fileName?: string` - Local filename (must end with .png or .svg, optional if returnBuffer is true)
|
|
156
268
|
- `format?: 'png' | 'svg'` - Image format to download (default: 'png')
|
|
157
269
|
- `pngScale?: number` - Export scale for PNG images (default: 2)
|
|
270
|
+
- `returnBuffer?: boolean` - Return image as ArrayBuffer instead of saving to disk (default: false)
|
|
271
|
+
- `enableLogging?: boolean` - Enable JSON debug log files (default: false)
|
|
158
272
|
|
|
159
273
|
**Returns:** Promise<FigmaImageResult>
|
|
160
274
|
|
|
275
|
+
**Result Properties:**
|
|
276
|
+
- `filePath?: string` - Path to saved file (only when returnBuffer is false)
|
|
277
|
+
- `buffer?: ArrayBuffer` - Image data as ArrayBuffer (only when returnBuffer is true)
|
|
278
|
+
- `finalDimensions: { width: number; height: number }` - Image dimensions
|
|
279
|
+
- `wasCropped: boolean` - Whether the image was cropped
|
|
280
|
+
- `cssVariables?: string` - CSS variables for dimensions (if requested)
|
|
281
|
+
|
|
161
282
|
## Authentication
|
|
162
283
|
|
|
163
284
|
You need either a Figma API key or OAuth token:
|
|
@@ -184,6 +305,7 @@ import { downloadFigmaFrameImage } from 'figma-metadata-extractor';
|
|
|
184
305
|
// Copy this URL from Figma when viewing a frame
|
|
185
306
|
const figmaUrl = 'https://www.figma.com/design/ABC123/My-Design?node-id=1234-5678&t=xyz123';
|
|
186
307
|
|
|
308
|
+
// Save to disk
|
|
187
309
|
const result = await downloadFigmaFrameImage(figmaUrl, {
|
|
188
310
|
apiKey: 'your-figma-api-key',
|
|
189
311
|
localPath: './downloads',
|
|
@@ -196,6 +318,29 @@ console.log(`Downloaded to: ${result.filePath}`);
|
|
|
196
318
|
console.log(`Dimensions: ${result.finalDimensions.width}x${result.finalDimensions.height}`);
|
|
197
319
|
```
|
|
198
320
|
|
|
321
|
+
### Get Frame Image as ArrayBuffer (No Disk Write)
|
|
322
|
+
|
|
323
|
+
If you want to process the image in memory without saving to disk:
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { downloadFigmaFrameImage } from 'figma-metadata-extractor';
|
|
327
|
+
|
|
328
|
+
const figmaUrl = 'https://www.figma.com/design/ABC123/My-Design?node-id=1234-5678';
|
|
329
|
+
|
|
330
|
+
// Get as ArrayBuffer
|
|
331
|
+
const result = await downloadFigmaFrameImage(figmaUrl, {
|
|
332
|
+
apiKey: 'your-figma-api-key',
|
|
333
|
+
returnBuffer: true,
|
|
334
|
+
format: 'png'
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
console.log(`Buffer size: ${result.buffer.byteLength} bytes`);
|
|
338
|
+
console.log(`Dimensions: ${result.finalDimensions.width}x${result.finalDimensions.height}`);
|
|
339
|
+
|
|
340
|
+
// Use the buffer directly (e.g., upload to cloud storage, process with sharp, etc.)
|
|
341
|
+
// const processedImage = await sharp(Buffer.from(result.buffer)).resize(100, 100).toBuffer();
|
|
342
|
+
```
|
|
343
|
+
|
|
199
344
|
### Download Multiple Frame Images
|
|
200
345
|
|
|
201
346
|
```typescript
|
|
@@ -216,6 +361,31 @@ const results = await downloadFigmaImages(
|
|
|
216
361
|
);
|
|
217
362
|
```
|
|
218
363
|
|
|
364
|
+
### Download Multiple Images as Buffers
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { downloadFigmaImages } from 'figma-metadata-extractor';
|
|
368
|
+
|
|
369
|
+
// Get multiple images as ArrayBuffers
|
|
370
|
+
const results = await downloadFigmaImages(
|
|
371
|
+
'https://figma.com/file/ABC123/My-Design',
|
|
372
|
+
[
|
|
373
|
+
{ nodeId: '1234:5678', fileName: 'frame1.png' },
|
|
374
|
+
{ nodeId: '9876:5432', fileName: 'frame2.png' }
|
|
375
|
+
],
|
|
376
|
+
{
|
|
377
|
+
apiKey: 'your-figma-api-key',
|
|
378
|
+
returnBuffer: true
|
|
379
|
+
}
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Process each buffer
|
|
383
|
+
results.forEach((result, index) => {
|
|
384
|
+
console.log(`Image ${index}: ${result.buffer.byteLength} bytes`);
|
|
385
|
+
// Upload to S3, process with sharp, etc.
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
219
389
|
## Advanced Usage
|
|
220
390
|
|
|
221
391
|
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
|
|
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}`);
|
|
@@ -302,17 +360,21 @@ class FigmaService {
|
|
|
302
360
|
* - PNG vs SVG format (based on filename extension)
|
|
303
361
|
* - Image cropping based on transform matrices
|
|
304
362
|
* - CSS variable generation for image dimensions
|
|
363
|
+
* - Returning as ArrayBuffer instead of saving to disk
|
|
305
364
|
*
|
|
306
365
|
* @returns Array of local file paths for successfully downloaded images
|
|
307
366
|
*/
|
|
308
367
|
async downloadImages(fileKey, localPath, items, options = {}) {
|
|
309
368
|
if (items.length === 0) return [];
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
if (!
|
|
313
|
-
|
|
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
|
+
}
|
|
314
377
|
}
|
|
315
|
-
const { pngScale = 2, svgOptions } = options;
|
|
316
378
|
const downloadPromises = [];
|
|
317
379
|
const imageFills = items.filter(
|
|
318
380
|
(item) => !!item.imageRef
|
|
@@ -330,7 +392,8 @@ class FigmaService {
|
|
|
330
392
|
imageUrl,
|
|
331
393
|
needsCropping,
|
|
332
394
|
cropTransform,
|
|
333
|
-
requiresImageDimensions
|
|
395
|
+
requiresImageDimensions,
|
|
396
|
+
returnBuffer
|
|
334
397
|
) : null;
|
|
335
398
|
}).filter((promise) => promise !== null);
|
|
336
399
|
if (fillDownloads.length > 0) {
|
|
@@ -355,7 +418,8 @@ class FigmaService {
|
|
|
355
418
|
imageUrl,
|
|
356
419
|
needsCropping,
|
|
357
420
|
cropTransform,
|
|
358
|
-
requiresImageDimensions
|
|
421
|
+
requiresImageDimensions,
|
|
422
|
+
returnBuffer
|
|
359
423
|
) : null;
|
|
360
424
|
}).filter((promise) => promise !== null);
|
|
361
425
|
if (pngDownloads.length > 0) {
|
|
@@ -377,7 +441,8 @@ class FigmaService {
|
|
|
377
441
|
imageUrl,
|
|
378
442
|
needsCropping,
|
|
379
443
|
cropTransform,
|
|
380
|
-
requiresImageDimensions
|
|
444
|
+
requiresImageDimensions,
|
|
445
|
+
returnBuffer
|
|
381
446
|
) : null;
|
|
382
447
|
}).filter((promise) => promise !== null);
|
|
383
448
|
if (svgDownloads.length > 0) {
|
|
@@ -411,23 +476,27 @@ class FigmaService {
|
|
|
411
476
|
return response;
|
|
412
477
|
}
|
|
413
478
|
}
|
|
414
|
-
async function downloadFigmaImage(fileName, localPath, imageUrl) {
|
|
479
|
+
async function downloadFigmaImage(fileName, localPath, imageUrl, returnBuffer = false) {
|
|
415
480
|
try {
|
|
416
|
-
if (!fs.existsSync(localPath)) {
|
|
417
|
-
fs.mkdirSync(localPath, { recursive: true });
|
|
418
|
-
}
|
|
419
|
-
const fullPath = path.join(localPath, fileName);
|
|
420
481
|
const response = await fetch(imageUrl, {
|
|
421
482
|
method: "GET"
|
|
422
483
|
});
|
|
423
484
|
if (!response.ok) {
|
|
424
485
|
throw new Error(`Failed to download image: ${response.statusText}`);
|
|
425
486
|
}
|
|
426
|
-
|
|
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);
|
|
427
495
|
const reader = response.body?.getReader();
|
|
428
496
|
if (!reader) {
|
|
429
497
|
throw new Error("Failed to get response body");
|
|
430
498
|
}
|
|
499
|
+
const writer = fs.createWriteStream(fullPath);
|
|
431
500
|
return new Promise((resolve, reject) => {
|
|
432
501
|
const processStream = async () => {
|
|
433
502
|
try {
|
|
@@ -1378,7 +1447,8 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1378
1447
|
imageFormat = "png",
|
|
1379
1448
|
pngScale = 2,
|
|
1380
1449
|
useRelativePaths = true,
|
|
1381
|
-
enableLogging = false
|
|
1450
|
+
enableLogging = false,
|
|
1451
|
+
returnBuffer = false
|
|
1382
1452
|
} = options;
|
|
1383
1453
|
Logger.enableLogging = enableLogging;
|
|
1384
1454
|
if (!apiKey && !oauthToken) {
|
|
@@ -1420,8 +1490,8 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1420
1490
|
globalVars
|
|
1421
1491
|
};
|
|
1422
1492
|
if (downloadImages) {
|
|
1423
|
-
if (!localPath) {
|
|
1424
|
-
throw new Error("localPath is required when downloadImages is true");
|
|
1493
|
+
if (!returnBuffer && !localPath) {
|
|
1494
|
+
throw new Error("localPath is required when downloadImages is true and returnBuffer is false");
|
|
1425
1495
|
}
|
|
1426
1496
|
Logger.log("Discovering and downloading image assets...");
|
|
1427
1497
|
const imageAssets = findImageAssets(nodes, globalVars);
|
|
@@ -1433,12 +1503,20 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1433
1503
|
}));
|
|
1434
1504
|
const downloadResults = await figmaService.downloadImages(
|
|
1435
1505
|
fileKey,
|
|
1436
|
-
localPath,
|
|
1506
|
+
localPath || "",
|
|
1437
1507
|
imageNodes,
|
|
1438
|
-
{
|
|
1508
|
+
{
|
|
1509
|
+
pngScale: imageFormat === "png" ? pngScale : void 0,
|
|
1510
|
+
returnBuffer
|
|
1511
|
+
}
|
|
1439
1512
|
);
|
|
1440
|
-
|
|
1441
|
-
|
|
1513
|
+
if (returnBuffer) {
|
|
1514
|
+
result.images = downloadResults;
|
|
1515
|
+
Logger.log(`Successfully downloaded ${downloadResults.length} images as buffers`);
|
|
1516
|
+
} else {
|
|
1517
|
+
result.nodes = enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths, localPath);
|
|
1518
|
+
Logger.log(`Successfully downloaded and enriched ${downloadResults.length} images`);
|
|
1519
|
+
}
|
|
1442
1520
|
}
|
|
1443
1521
|
}
|
|
1444
1522
|
if (outputFormat === "json") {
|
|
@@ -1455,11 +1533,14 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1455
1533
|
}
|
|
1456
1534
|
}
|
|
1457
1535
|
async function downloadFigmaImages(figmaUrl, nodes, options) {
|
|
1458
|
-
const { apiKey, oauthToken, useOAuth = false, pngScale = 2, localPath, enableLogging = false } = options;
|
|
1536
|
+
const { apiKey, oauthToken, useOAuth = false, pngScale = 2, localPath, enableLogging = false, returnBuffer = false } = options;
|
|
1459
1537
|
Logger.enableLogging = enableLogging;
|
|
1460
1538
|
if (!apiKey && !oauthToken) {
|
|
1461
1539
|
throw new Error("Either apiKey or oauthToken is required");
|
|
1462
1540
|
}
|
|
1541
|
+
if (!returnBuffer && !localPath) {
|
|
1542
|
+
throw new Error("localPath is required when returnBuffer is false");
|
|
1543
|
+
}
|
|
1463
1544
|
const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
|
|
1464
1545
|
if (!urlMatch) {
|
|
1465
1546
|
throw new Error("Invalid Figma URL format");
|
|
@@ -1475,8 +1556,9 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
|
|
|
1475
1556
|
...node,
|
|
1476
1557
|
nodeId: node.nodeId.replace(/-/g, ":")
|
|
1477
1558
|
}));
|
|
1478
|
-
const results = await figmaService.downloadImages(fileKey, localPath, processedNodes, {
|
|
1479
|
-
pngScale
|
|
1559
|
+
const results = await figmaService.downloadImages(fileKey, localPath || "", processedNodes, {
|
|
1560
|
+
pngScale,
|
|
1561
|
+
returnBuffer
|
|
1480
1562
|
});
|
|
1481
1563
|
return results;
|
|
1482
1564
|
} catch (error) {
|
|
@@ -1493,12 +1575,16 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1493
1575
|
localPath,
|
|
1494
1576
|
fileName,
|
|
1495
1577
|
format = "png",
|
|
1496
|
-
enableLogging = false
|
|
1578
|
+
enableLogging = false,
|
|
1579
|
+
returnBuffer = false
|
|
1497
1580
|
} = options;
|
|
1498
1581
|
Logger.enableLogging = enableLogging;
|
|
1499
1582
|
if (!apiKey && !oauthToken) {
|
|
1500
1583
|
throw new Error("Either apiKey or oauthToken is required");
|
|
1501
1584
|
}
|
|
1585
|
+
if (!returnBuffer && (!localPath || !fileName)) {
|
|
1586
|
+
throw new Error("localPath and fileName are required when returnBuffer is false");
|
|
1587
|
+
}
|
|
1502
1588
|
const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
|
|
1503
1589
|
if (!urlMatch) {
|
|
1504
1590
|
throw new Error("Invalid Figma URL format");
|
|
@@ -1509,9 +1595,11 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1509
1595
|
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)");
|
|
1510
1596
|
}
|
|
1511
1597
|
const nodeId = nodeIdMatch[1].replace(/-/g, ":");
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1598
|
+
if (fileName) {
|
|
1599
|
+
const expectedExtension = `.${format}`;
|
|
1600
|
+
if (!fileName.toLowerCase().endsWith(expectedExtension)) {
|
|
1601
|
+
throw new Error(`Filename must end with ${expectedExtension} for ${format} format`);
|
|
1602
|
+
}
|
|
1515
1603
|
}
|
|
1516
1604
|
const figmaService = new FigmaService({
|
|
1517
1605
|
figmaApiKey: apiKey || "",
|
|
@@ -1522,15 +1610,20 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1522
1610
|
Logger.log(`Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`);
|
|
1523
1611
|
const imageNode = {
|
|
1524
1612
|
nodeId,
|
|
1525
|
-
fileName
|
|
1613
|
+
fileName: fileName || `temp.${format}`
|
|
1526
1614
|
};
|
|
1527
|
-
const results = await figmaService.downloadImages(fileKey, localPath, [imageNode], {
|
|
1528
|
-
pngScale: format === "png" ? pngScale : void 0
|
|
1615
|
+
const results = await figmaService.downloadImages(fileKey, localPath || "", [imageNode], {
|
|
1616
|
+
pngScale: format === "png" ? pngScale : void 0,
|
|
1617
|
+
returnBuffer
|
|
1529
1618
|
});
|
|
1530
1619
|
if (results.length === 0) {
|
|
1531
1620
|
throw new Error(`Failed to download image for frame ${nodeId}`);
|
|
1532
1621
|
}
|
|
1533
|
-
|
|
1622
|
+
if (returnBuffer) {
|
|
1623
|
+
Logger.log(`Successfully downloaded frame image as buffer`);
|
|
1624
|
+
} else {
|
|
1625
|
+
Logger.log(`Successfully downloaded frame image to: ${results[0].filePath}`);
|
|
1626
|
+
}
|
|
1534
1627
|
return results[0];
|
|
1535
1628
|
} catch (error) {
|
|
1536
1629
|
Logger.error(`Error downloading frame image from ${fileKey}:`, error);
|
|
@@ -1568,7 +1661,7 @@ function enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativeP
|
|
|
1568
1661
|
const imageMap = /* @__PURE__ */ new Map();
|
|
1569
1662
|
imageAssets.forEach((asset, index) => {
|
|
1570
1663
|
const result = downloadResults[index];
|
|
1571
|
-
if (result) {
|
|
1664
|
+
if (result && result.filePath) {
|
|
1572
1665
|
let pathForMarkup;
|
|
1573
1666
|
if (useRelativePaths === false) {
|
|
1574
1667
|
pathForMarkup = result.filePath;
|
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
|
|
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}`);
|
|
@@ -300,17 +358,21 @@ class FigmaService {
|
|
|
300
358
|
* - PNG vs SVG format (based on filename extension)
|
|
301
359
|
* - Image cropping based on transform matrices
|
|
302
360
|
* - CSS variable generation for image dimensions
|
|
361
|
+
* - Returning as ArrayBuffer instead of saving to disk
|
|
303
362
|
*
|
|
304
363
|
* @returns Array of local file paths for successfully downloaded images
|
|
305
364
|
*/
|
|
306
365
|
async downloadImages(fileKey, localPath, items, options = {}) {
|
|
307
366
|
if (items.length === 0) return [];
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
if (!
|
|
311
|
-
|
|
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
|
+
}
|
|
312
375
|
}
|
|
313
|
-
const { pngScale = 2, svgOptions } = options;
|
|
314
376
|
const downloadPromises = [];
|
|
315
377
|
const imageFills = items.filter(
|
|
316
378
|
(item) => !!item.imageRef
|
|
@@ -328,7 +390,8 @@ class FigmaService {
|
|
|
328
390
|
imageUrl,
|
|
329
391
|
needsCropping,
|
|
330
392
|
cropTransform,
|
|
331
|
-
requiresImageDimensions
|
|
393
|
+
requiresImageDimensions,
|
|
394
|
+
returnBuffer
|
|
332
395
|
) : null;
|
|
333
396
|
}).filter((promise) => promise !== null);
|
|
334
397
|
if (fillDownloads.length > 0) {
|
|
@@ -353,7 +416,8 @@ class FigmaService {
|
|
|
353
416
|
imageUrl,
|
|
354
417
|
needsCropping,
|
|
355
418
|
cropTransform,
|
|
356
|
-
requiresImageDimensions
|
|
419
|
+
requiresImageDimensions,
|
|
420
|
+
returnBuffer
|
|
357
421
|
) : null;
|
|
358
422
|
}).filter((promise) => promise !== null);
|
|
359
423
|
if (pngDownloads.length > 0) {
|
|
@@ -375,7 +439,8 @@ class FigmaService {
|
|
|
375
439
|
imageUrl,
|
|
376
440
|
needsCropping,
|
|
377
441
|
cropTransform,
|
|
378
|
-
requiresImageDimensions
|
|
442
|
+
requiresImageDimensions,
|
|
443
|
+
returnBuffer
|
|
379
444
|
) : null;
|
|
380
445
|
}).filter((promise) => promise !== null);
|
|
381
446
|
if (svgDownloads.length > 0) {
|
|
@@ -409,23 +474,27 @@ class FigmaService {
|
|
|
409
474
|
return response;
|
|
410
475
|
}
|
|
411
476
|
}
|
|
412
|
-
async function downloadFigmaImage(fileName, localPath, imageUrl) {
|
|
477
|
+
async function downloadFigmaImage(fileName, localPath, imageUrl, returnBuffer = false) {
|
|
413
478
|
try {
|
|
414
|
-
if (!fs.existsSync(localPath)) {
|
|
415
|
-
fs.mkdirSync(localPath, { recursive: true });
|
|
416
|
-
}
|
|
417
|
-
const fullPath = path.join(localPath, fileName);
|
|
418
479
|
const response = await fetch(imageUrl, {
|
|
419
480
|
method: "GET"
|
|
420
481
|
});
|
|
421
482
|
if (!response.ok) {
|
|
422
483
|
throw new Error(`Failed to download image: ${response.statusText}`);
|
|
423
484
|
}
|
|
424
|
-
|
|
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);
|
|
425
493
|
const reader = response.body?.getReader();
|
|
426
494
|
if (!reader) {
|
|
427
495
|
throw new Error("Failed to get response body");
|
|
428
496
|
}
|
|
497
|
+
const writer = fs.createWriteStream(fullPath);
|
|
429
498
|
return new Promise((resolve, reject) => {
|
|
430
499
|
const processStream = async () => {
|
|
431
500
|
try {
|
|
@@ -1376,7 +1445,8 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1376
1445
|
imageFormat = "png",
|
|
1377
1446
|
pngScale = 2,
|
|
1378
1447
|
useRelativePaths = true,
|
|
1379
|
-
enableLogging = false
|
|
1448
|
+
enableLogging = false,
|
|
1449
|
+
returnBuffer = false
|
|
1380
1450
|
} = options;
|
|
1381
1451
|
Logger.enableLogging = enableLogging;
|
|
1382
1452
|
if (!apiKey && !oauthToken) {
|
|
@@ -1418,8 +1488,8 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1418
1488
|
globalVars
|
|
1419
1489
|
};
|
|
1420
1490
|
if (downloadImages) {
|
|
1421
|
-
if (!localPath) {
|
|
1422
|
-
throw new Error("localPath is required when downloadImages is true");
|
|
1491
|
+
if (!returnBuffer && !localPath) {
|
|
1492
|
+
throw new Error("localPath is required when downloadImages is true and returnBuffer is false");
|
|
1423
1493
|
}
|
|
1424
1494
|
Logger.log("Discovering and downloading image assets...");
|
|
1425
1495
|
const imageAssets = findImageAssets(nodes, globalVars);
|
|
@@ -1431,12 +1501,20 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1431
1501
|
}));
|
|
1432
1502
|
const downloadResults = await figmaService.downloadImages(
|
|
1433
1503
|
fileKey,
|
|
1434
|
-
localPath,
|
|
1504
|
+
localPath || "",
|
|
1435
1505
|
imageNodes,
|
|
1436
|
-
{
|
|
1506
|
+
{
|
|
1507
|
+
pngScale: imageFormat === "png" ? pngScale : void 0,
|
|
1508
|
+
returnBuffer
|
|
1509
|
+
}
|
|
1437
1510
|
);
|
|
1438
|
-
|
|
1439
|
-
|
|
1511
|
+
if (returnBuffer) {
|
|
1512
|
+
result.images = downloadResults;
|
|
1513
|
+
Logger.log(`Successfully downloaded ${downloadResults.length} images as buffers`);
|
|
1514
|
+
} else {
|
|
1515
|
+
result.nodes = enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths, localPath);
|
|
1516
|
+
Logger.log(`Successfully downloaded and enriched ${downloadResults.length} images`);
|
|
1517
|
+
}
|
|
1440
1518
|
}
|
|
1441
1519
|
}
|
|
1442
1520
|
if (outputFormat === "json") {
|
|
@@ -1453,11 +1531,14 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1453
1531
|
}
|
|
1454
1532
|
}
|
|
1455
1533
|
async function downloadFigmaImages(figmaUrl, nodes, options) {
|
|
1456
|
-
const { apiKey, oauthToken, useOAuth = false, pngScale = 2, localPath, enableLogging = false } = options;
|
|
1534
|
+
const { apiKey, oauthToken, useOAuth = false, pngScale = 2, localPath, enableLogging = false, returnBuffer = false } = options;
|
|
1457
1535
|
Logger.enableLogging = enableLogging;
|
|
1458
1536
|
if (!apiKey && !oauthToken) {
|
|
1459
1537
|
throw new Error("Either apiKey or oauthToken is required");
|
|
1460
1538
|
}
|
|
1539
|
+
if (!returnBuffer && !localPath) {
|
|
1540
|
+
throw new Error("localPath is required when returnBuffer is false");
|
|
1541
|
+
}
|
|
1461
1542
|
const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
|
|
1462
1543
|
if (!urlMatch) {
|
|
1463
1544
|
throw new Error("Invalid Figma URL format");
|
|
@@ -1473,8 +1554,9 @@ async function downloadFigmaImages(figmaUrl, nodes, options) {
|
|
|
1473
1554
|
...node,
|
|
1474
1555
|
nodeId: node.nodeId.replace(/-/g, ":")
|
|
1475
1556
|
}));
|
|
1476
|
-
const results = await figmaService.downloadImages(fileKey, localPath, processedNodes, {
|
|
1477
|
-
pngScale
|
|
1557
|
+
const results = await figmaService.downloadImages(fileKey, localPath || "", processedNodes, {
|
|
1558
|
+
pngScale,
|
|
1559
|
+
returnBuffer
|
|
1478
1560
|
});
|
|
1479
1561
|
return results;
|
|
1480
1562
|
} catch (error) {
|
|
@@ -1491,12 +1573,16 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1491
1573
|
localPath,
|
|
1492
1574
|
fileName,
|
|
1493
1575
|
format = "png",
|
|
1494
|
-
enableLogging = false
|
|
1576
|
+
enableLogging = false,
|
|
1577
|
+
returnBuffer = false
|
|
1495
1578
|
} = options;
|
|
1496
1579
|
Logger.enableLogging = enableLogging;
|
|
1497
1580
|
if (!apiKey && !oauthToken) {
|
|
1498
1581
|
throw new Error("Either apiKey or oauthToken is required");
|
|
1499
1582
|
}
|
|
1583
|
+
if (!returnBuffer && (!localPath || !fileName)) {
|
|
1584
|
+
throw new Error("localPath and fileName are required when returnBuffer is false");
|
|
1585
|
+
}
|
|
1500
1586
|
const urlMatch = figmaUrl.match(/figma\.com\/(file|design)\/([a-zA-Z0-9]+)/);
|
|
1501
1587
|
if (!urlMatch) {
|
|
1502
1588
|
throw new Error("Invalid Figma URL format");
|
|
@@ -1507,9 +1593,11 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1507
1593
|
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)");
|
|
1508
1594
|
}
|
|
1509
1595
|
const nodeId = nodeIdMatch[1].replace(/-/g, ":");
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1596
|
+
if (fileName) {
|
|
1597
|
+
const expectedExtension = `.${format}`;
|
|
1598
|
+
if (!fileName.toLowerCase().endsWith(expectedExtension)) {
|
|
1599
|
+
throw new Error(`Filename must end with ${expectedExtension} for ${format} format`);
|
|
1600
|
+
}
|
|
1513
1601
|
}
|
|
1514
1602
|
const figmaService = new FigmaService({
|
|
1515
1603
|
figmaApiKey: apiKey || "",
|
|
@@ -1520,15 +1608,20 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1520
1608
|
Logger.log(`Downloading ${format.toUpperCase()} image for frame ${nodeId} from file ${fileKey}`);
|
|
1521
1609
|
const imageNode = {
|
|
1522
1610
|
nodeId,
|
|
1523
|
-
fileName
|
|
1611
|
+
fileName: fileName || `temp.${format}`
|
|
1524
1612
|
};
|
|
1525
|
-
const results = await figmaService.downloadImages(fileKey, localPath, [imageNode], {
|
|
1526
|
-
pngScale: format === "png" ? pngScale : void 0
|
|
1613
|
+
const results = await figmaService.downloadImages(fileKey, localPath || "", [imageNode], {
|
|
1614
|
+
pngScale: format === "png" ? pngScale : void 0,
|
|
1615
|
+
returnBuffer
|
|
1527
1616
|
});
|
|
1528
1617
|
if (results.length === 0) {
|
|
1529
1618
|
throw new Error(`Failed to download image for frame ${nodeId}`);
|
|
1530
1619
|
}
|
|
1531
|
-
|
|
1620
|
+
if (returnBuffer) {
|
|
1621
|
+
Logger.log(`Successfully downloaded frame image as buffer`);
|
|
1622
|
+
} else {
|
|
1623
|
+
Logger.log(`Successfully downloaded frame image to: ${results[0].filePath}`);
|
|
1624
|
+
}
|
|
1532
1625
|
return results[0];
|
|
1533
1626
|
} catch (error) {
|
|
1534
1627
|
Logger.error(`Error downloading frame image from ${fileKey}:`, error);
|
|
@@ -1566,7 +1659,7 @@ function enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativeP
|
|
|
1566
1659
|
const imageMap = /* @__PURE__ */ new Map();
|
|
1567
1660
|
imageAssets.forEach((asset, index) => {
|
|
1568
1661
|
const result = downloadResults[index];
|
|
1569
|
-
if (result) {
|
|
1662
|
+
if (result && result.filePath) {
|
|
1570
1663
|
let pathForMarkup;
|
|
1571
1664
|
if (useRelativePaths === false) {
|
|
1572
1665
|
pathForMarkup = result.filePath;
|
package/dist/lib.d.ts
CHANGED
|
@@ -27,12 +27,16 @@ export interface FigmaMetadataOptions {
|
|
|
27
27
|
useRelativePaths?: boolean | string;
|
|
28
28
|
/** Enable JSON debug log files (defaults to false) */
|
|
29
29
|
enableLogging?: boolean;
|
|
30
|
+
/** Return images as ArrayBuffer instead of saving to disk (defaults to false) */
|
|
31
|
+
returnBuffer?: boolean;
|
|
30
32
|
}
|
|
31
33
|
export interface FigmaImageOptions {
|
|
32
34
|
/** Export scale for PNG images (defaults to 2) */
|
|
33
35
|
pngScale?: number;
|
|
34
|
-
/** The absolute path to the directory where images should be stored */
|
|
35
|
-
localPath
|
|
36
|
+
/** The absolute path to the directory where images should be stored (optional if returnBuffer is true) */
|
|
37
|
+
localPath?: string;
|
|
38
|
+
/** Return images as ArrayBuffer instead of saving to disk (defaults to false) */
|
|
39
|
+
returnBuffer?: boolean;
|
|
36
40
|
}
|
|
37
41
|
export interface FigmaImageNode {
|
|
38
42
|
/** The ID of the Figma node, formatted as '1234:5678' */
|
|
@@ -54,9 +58,11 @@ export interface FigmaMetadataResult {
|
|
|
54
58
|
metadata: any;
|
|
55
59
|
nodes: any[];
|
|
56
60
|
globalVars: any;
|
|
61
|
+
images?: FigmaImageResult[];
|
|
57
62
|
}
|
|
58
63
|
export interface FigmaImageResult {
|
|
59
|
-
filePath
|
|
64
|
+
filePath?: string;
|
|
65
|
+
buffer?: ArrayBuffer;
|
|
60
66
|
finalDimensions: {
|
|
61
67
|
width: number;
|
|
62
68
|
height: number;
|
|
@@ -73,14 +79,16 @@ export interface FigmaFrameImageOptions {
|
|
|
73
79
|
useOAuth?: boolean;
|
|
74
80
|
/** Export scale for PNG images (defaults to 2) */
|
|
75
81
|
pngScale?: number;
|
|
76
|
-
/** The absolute path to the directory where the image should be stored */
|
|
77
|
-
localPath
|
|
78
|
-
/** The filename for the downloaded image (must end with .png or .svg) */
|
|
79
|
-
fileName
|
|
82
|
+
/** The absolute path to the directory where the image should be stored (optional if returnBuffer is true) */
|
|
83
|
+
localPath?: string;
|
|
84
|
+
/** The filename for the downloaded image (must end with .png or .svg, optional if returnBuffer is true) */
|
|
85
|
+
fileName?: string;
|
|
80
86
|
/** Image format to download (defaults to 'png') */
|
|
81
87
|
format?: 'png' | 'svg';
|
|
82
88
|
/** Enable JSON debug log files (defaults to false) */
|
|
83
89
|
enableLogging?: boolean;
|
|
90
|
+
/** Return image as ArrayBuffer instead of saving to disk (defaults to false) */
|
|
91
|
+
returnBuffer?: boolean;
|
|
84
92
|
}
|
|
85
93
|
/**
|
|
86
94
|
* Extract metadata from a Figma file or specific nodes
|
|
@@ -107,6 +115,20 @@ export declare function downloadFigmaImages(figmaUrl: string, nodes: FigmaImageN
|
|
|
107
115
|
* @returns Promise resolving to the download result
|
|
108
116
|
*/
|
|
109
117
|
export declare function downloadFigmaFrameImage(figmaUrl: string, options: FigmaFrameImageOptions): Promise<FigmaImageResult>;
|
|
118
|
+
/**
|
|
119
|
+
* Enrich metadata with saved image file paths
|
|
120
|
+
*
|
|
121
|
+
* Use this function after saving images from buffers to disk to add file path information to the metadata.
|
|
122
|
+
*
|
|
123
|
+
* @param metadata - The metadata result from getFigmaMetadata
|
|
124
|
+
* @param imagePaths - Array of file paths corresponding to the images array
|
|
125
|
+
* @param options - Options for path generation
|
|
126
|
+
* @returns Enriched metadata with downloadedImage properties on nodes
|
|
127
|
+
*/
|
|
128
|
+
export declare function enrichMetadataWithImages(metadata: FigmaMetadataResult, imagePaths: string[], options?: {
|
|
129
|
+
useRelativePaths?: boolean | string;
|
|
130
|
+
localPath?: string;
|
|
131
|
+
}): FigmaMetadataResult;
|
|
110
132
|
export type { SimplifiedDesign } from "./extractors/types.js";
|
|
111
133
|
export type { ExtractorFn, TraversalContext, TraversalOptions, GlobalVars, StyleTypes, } from "./extractors/index.js";
|
|
112
134
|
export { extractFromDesign, simplifyRawFigmaObject, layoutExtractor, textExtractor, visualsExtractor, componentExtractor, allExtractors, layoutAndText, contentOnly, visualsOnly, layoutOnly, collapseSvgContainers, } from "./extractors/index.js";
|
package/dist/services/figma.d.ts
CHANGED
|
@@ -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)
|
package/dist/utils/common.d.ts
CHANGED
|
@@ -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
|
-
* @
|
|
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
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-metadata-extractor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
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",
|