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 +65 -3
- package/dist/index.cjs +119 -28
- package/dist/index.js +119 -28
- package/dist/lib.d.ts +16 -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/dist/utils/logger.d.ts +1 -0
- package/package.json +1 -1
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
|
|
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
|
|
155
|
-
- `fileName
|
|
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
|
|
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
|
|
309
|
-
|
|
310
|
-
if (!
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
307
|
-
|
|
308
|
-
if (!
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
76
|
-
/** The filename for the downloaded image (must end with .png or .svg) */
|
|
77
|
-
fileName
|
|
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
|
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/dist/utils/logger.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-metadata-extractor",
|
|
3
|
-
"version": "1.0.
|
|
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",
|