figma-metadata-extractor 1.0.15 → 1.0.16

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.
Files changed (3) hide show
  1. package/dist/index.cjs +85 -148
  2. package/dist/index.js +85 -148
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -499,143 +499,67 @@ class FigmaService {
499
499
  );
500
500
  }
501
501
  }
502
- const downloadPromises = [];
503
- const imageFills = items.filter(
504
- (item) => !!item.imageRef
505
- );
506
- const renderNodes = items.filter(
507
- (item) => !!item.nodeId
508
- );
509
- if (imageFills.length > 0) {
510
- const fillUrls = await this.getImageFillUrls(fileKey);
511
- const fillTasks = imageFills.map(
512
- ({
513
- imageRef,
514
- fileName,
515
- needsCropping,
516
- cropTransform,
517
- requiresImageDimensions
518
- }) => {
519
- const imageUrl = fillUrls[imageRef];
520
- if (!imageUrl) {
521
- Logger.log(
522
- `Skipping image fill with missing URL for imageRef: ${imageRef}`
523
- );
524
- return null;
525
- }
526
- return () => downloadAndProcessImage(
527
- fileName,
528
- resolvedPath,
529
- imageUrl,
530
- needsCropping,
531
- cropTransform,
532
- requiresImageDimensions,
533
- returnBuffer
534
- );
502
+ const imageRefs = [
503
+ ...new Set(items.map((i) => i.imageRef).filter((r) => !!r))
504
+ ];
505
+ const pngNodeIds = items.filter(
506
+ (i) => !i.imageRef && i.nodeId && !i.fileName.toLowerCase().endsWith(".svg")
507
+ ).map((i) => i.nodeId);
508
+ const svgNodeIds = items.filter(
509
+ (i) => !i.imageRef && i.nodeId && i.fileName.toLowerCase().endsWith(".svg")
510
+ ).map((i) => i.nodeId);
511
+ const [fillUrls, pngUrls, svgUrls] = await Promise.all([
512
+ imageRefs.length > 0 ? this.getImageFillUrls(fileKey) : Promise.resolve({}),
513
+ pngNodeIds.length > 0 ? this.getNodeRenderUrls(fileKey, pngNodeIds, "png", { pngScale }) : Promise.resolve({}),
514
+ svgNodeIds.length > 0 ? this.getNodeRenderUrls(fileKey, svgNodeIds, "svg", { svgOptions }) : Promise.resolve({})
515
+ ]);
516
+ const tasks = items.map((item) => {
517
+ const {
518
+ imageRef,
519
+ nodeId,
520
+ fileName,
521
+ needsCropping,
522
+ cropTransform,
523
+ requiresImageDimensions
524
+ } = item;
525
+ let imageUrl;
526
+ if (imageRef) {
527
+ imageUrl = fillUrls[imageRef];
528
+ } else if (nodeId) {
529
+ if (fileName.toLowerCase().endsWith(".svg")) {
530
+ imageUrl = svgUrls[nodeId];
531
+ } else {
532
+ imageUrl = pngUrls[nodeId];
535
533
  }
536
- ).filter(
537
- (task) => task !== null
538
- );
539
- if (fillTasks.length > 0) {
540
- downloadPromises.push(runWithConcurrency(fillTasks, CONCURRENCY_LIMIT));
541
534
  }
542
- }
543
- if (renderNodes.length > 0) {
544
- const pngNodes = renderNodes.filter(
545
- (node) => !node.fileName.toLowerCase().endsWith(".svg")
546
- );
547
- const svgNodes = renderNodes.filter(
548
- (node) => node.fileName.toLowerCase().endsWith(".svg")
549
- );
550
- if (pngNodes.length > 0) {
551
- const pngUrls = await this.getNodeRenderUrls(
552
- fileKey,
553
- pngNodes.map((n) => n.nodeId),
554
- "png",
555
- { pngScale }
556
- );
557
- const pngTasks = pngNodes.map(
558
- ({
559
- nodeId,
560
- fileName,
561
- needsCropping,
562
- cropTransform,
563
- requiresImageDimensions
564
- }) => {
565
- const imageUrl = pngUrls[nodeId];
566
- if (!imageUrl) {
567
- Logger.log(
568
- `Skipping PNG render with missing URL for nodeId: ${nodeId}`
569
- );
570
- return null;
571
- }
572
- return () => downloadAndProcessImage(
573
- fileName,
574
- resolvedPath,
575
- imageUrl,
576
- needsCropping,
577
- cropTransform,
578
- requiresImageDimensions,
579
- returnBuffer
580
- ).then(
581
- (result) => ({ ...result, nodeId })
582
- );
583
- }
584
- ).filter(
585
- (task) => task !== null
586
- );
587
- if (pngTasks.length > 0) {
588
- downloadPromises.push(
589
- runWithConcurrency(pngTasks, CONCURRENCY_LIMIT)
535
+ if (!imageUrl) {
536
+ return () => {
537
+ Logger.log(
538
+ `Skipping missing image for ${imageRef ? `imageRef: ${imageRef}` : `nodeId: ${nodeId}`}`
590
539
  );
591
- }
592
- }
593
- if (svgNodes.length > 0) {
594
- const svgUrls = await this.getNodeRenderUrls(
595
- fileKey,
596
- svgNodes.map((n) => n.nodeId),
597
- "svg",
598
- { svgOptions }
599
- );
600
- const svgTasks = svgNodes.map(
601
- ({
540
+ return Promise.resolve({
602
541
  nodeId,
603
- fileName,
604
- needsCropping,
605
- cropTransform,
606
- requiresImageDimensions
607
- }) => {
608
- const imageUrl = svgUrls[nodeId];
609
- if (!imageUrl) {
610
- Logger.log(
611
- `Skipping SVG render with missing URL for nodeId: ${nodeId}`
612
- );
613
- return null;
614
- }
615
- return () => downloadAndProcessImage(
616
- fileName,
617
- resolvedPath,
618
- imageUrl,
619
- needsCropping,
620
- cropTransform,
621
- requiresImageDimensions,
622
- returnBuffer
623
- ).then(
624
- (result) => ({ ...result, nodeId })
625
- );
626
- }
627
- ).filter(
628
- (task) => task !== null
629
- );
630
- if (svgTasks.length > 0) {
631
- downloadPromises.push(
632
- runWithConcurrency(svgTasks, CONCURRENCY_LIMIT)
633
- );
634
- }
542
+ originalDimensions: { width: 0, height: 0 },
543
+ finalDimensions: { width: 0, height: 0 },
544
+ wasCropped: false,
545
+ processingLog: ["Skipped: URL not found"]
546
+ });
547
+ };
635
548
  }
636
- }
637
- const results = await Promise.all(downloadPromises);
638
- return results.flat();
549
+ return async () => {
550
+ const result = await downloadAndProcessImage(
551
+ fileName,
552
+ resolvedPath,
553
+ imageUrl,
554
+ needsCropping,
555
+ cropTransform,
556
+ requiresImageDimensions,
557
+ returnBuffer
558
+ );
559
+ return { ...result, nodeId };
560
+ };
561
+ });
562
+ return runWithConcurrency(tasks, CONCURRENCY_LIMIT);
639
563
  }
640
564
  /**
641
565
  * Get raw Figma API response for a file (for use with flexible extractors)
@@ -1596,7 +1520,11 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1596
1520
  if (imageAssets.length > 0) {
1597
1521
  const imageNodes = imageAssets.map((asset) => ({
1598
1522
  nodeId: asset.id,
1599
- fileName: sanitizeFileName(asset.name) + `.${imageFormat}`
1523
+ imageRef: asset.imageRef,
1524
+ fileName: sanitizeFileName(asset.name) + (asset.filenameSuffix ? `-${asset.filenameSuffix}` : "") + `.${imageFormat}`,
1525
+ needsCropping: asset.needsCropping,
1526
+ cropTransform: asset.cropTransform,
1527
+ requiresImageDimensions: asset.requiresImageDimensions
1600
1528
  }));
1601
1529
  const downloadResults = await figmaService.downloadImages(
1602
1530
  fileKey,
@@ -1831,28 +1759,37 @@ function sanitizeFileName(name) {
1831
1759
  return name.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
1832
1760
  }
1833
1761
  function findImageAssets(nodes, globalVars) {
1834
- const images = [];
1762
+ const assets = [];
1835
1763
  function traverse(node) {
1836
- const isImageAsset = node.type === "IMAGE-SVG" || hasImageFill(node, globalVars);
1837
- if (isImageAsset) {
1838
- images.push(node);
1764
+ if (node.type === "IMAGE-SVG") {
1765
+ assets.push({
1766
+ ...node,
1767
+ id: node.id,
1768
+ name: node.name,
1769
+ type: "render"
1770
+ });
1771
+ } else if (node.fills && typeof node.fills === "string") {
1772
+ const fillData = globalVars?.styles?.[node.fills];
1773
+ if (Array.isArray(fillData)) {
1774
+ const imageFill = fillData.find((f) => f.type === "IMAGE");
1775
+ if (imageFill) {
1776
+ assets.push({
1777
+ ...node,
1778
+ id: node.id,
1779
+ name: node.name,
1780
+ type: "fill",
1781
+ imageRef: imageFill.imageRef,
1782
+ ...imageFill.imageDownloadArguments
1783
+ });
1784
+ }
1785
+ }
1839
1786
  }
1840
1787
  if (node.children && Array.isArray(node.children)) {
1841
1788
  node.children.forEach(traverse);
1842
1789
  }
1843
1790
  }
1844
1791
  nodes.forEach(traverse);
1845
- return images;
1846
- }
1847
- function hasImageFill(node, globalVars) {
1848
- if (!node.fills || typeof node.fills !== "string") {
1849
- return false;
1850
- }
1851
- const fillData = globalVars?.styles?.[node.fills];
1852
- if (!fillData || !Array.isArray(fillData)) {
1853
- return false;
1854
- }
1855
- return fillData.some((fill) => fill?.type === "IMAGE");
1792
+ return assets;
1856
1793
  }
1857
1794
  function enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths = true, localPath) {
1858
1795
  const imageMap = /* @__PURE__ */ new Map();
package/dist/index.js CHANGED
@@ -497,143 +497,67 @@ class FigmaService {
497
497
  );
498
498
  }
499
499
  }
500
- const downloadPromises = [];
501
- const imageFills = items.filter(
502
- (item) => !!item.imageRef
503
- );
504
- const renderNodes = items.filter(
505
- (item) => !!item.nodeId
506
- );
507
- if (imageFills.length > 0) {
508
- const fillUrls = await this.getImageFillUrls(fileKey);
509
- const fillTasks = imageFills.map(
510
- ({
511
- imageRef,
512
- fileName,
513
- needsCropping,
514
- cropTransform,
515
- requiresImageDimensions
516
- }) => {
517
- const imageUrl = fillUrls[imageRef];
518
- if (!imageUrl) {
519
- Logger.log(
520
- `Skipping image fill with missing URL for imageRef: ${imageRef}`
521
- );
522
- return null;
523
- }
524
- return () => downloadAndProcessImage(
525
- fileName,
526
- resolvedPath,
527
- imageUrl,
528
- needsCropping,
529
- cropTransform,
530
- requiresImageDimensions,
531
- returnBuffer
532
- );
500
+ const imageRefs = [
501
+ ...new Set(items.map((i) => i.imageRef).filter((r) => !!r))
502
+ ];
503
+ const pngNodeIds = items.filter(
504
+ (i) => !i.imageRef && i.nodeId && !i.fileName.toLowerCase().endsWith(".svg")
505
+ ).map((i) => i.nodeId);
506
+ const svgNodeIds = items.filter(
507
+ (i) => !i.imageRef && i.nodeId && i.fileName.toLowerCase().endsWith(".svg")
508
+ ).map((i) => i.nodeId);
509
+ const [fillUrls, pngUrls, svgUrls] = await Promise.all([
510
+ imageRefs.length > 0 ? this.getImageFillUrls(fileKey) : Promise.resolve({}),
511
+ pngNodeIds.length > 0 ? this.getNodeRenderUrls(fileKey, pngNodeIds, "png", { pngScale }) : Promise.resolve({}),
512
+ svgNodeIds.length > 0 ? this.getNodeRenderUrls(fileKey, svgNodeIds, "svg", { svgOptions }) : Promise.resolve({})
513
+ ]);
514
+ const tasks = items.map((item) => {
515
+ const {
516
+ imageRef,
517
+ nodeId,
518
+ fileName,
519
+ needsCropping,
520
+ cropTransform,
521
+ requiresImageDimensions
522
+ } = item;
523
+ let imageUrl;
524
+ if (imageRef) {
525
+ imageUrl = fillUrls[imageRef];
526
+ } else if (nodeId) {
527
+ if (fileName.toLowerCase().endsWith(".svg")) {
528
+ imageUrl = svgUrls[nodeId];
529
+ } else {
530
+ imageUrl = pngUrls[nodeId];
533
531
  }
534
- ).filter(
535
- (task) => task !== null
536
- );
537
- if (fillTasks.length > 0) {
538
- downloadPromises.push(runWithConcurrency(fillTasks, CONCURRENCY_LIMIT));
539
532
  }
540
- }
541
- if (renderNodes.length > 0) {
542
- const pngNodes = renderNodes.filter(
543
- (node) => !node.fileName.toLowerCase().endsWith(".svg")
544
- );
545
- const svgNodes = renderNodes.filter(
546
- (node) => node.fileName.toLowerCase().endsWith(".svg")
547
- );
548
- if (pngNodes.length > 0) {
549
- const pngUrls = await this.getNodeRenderUrls(
550
- fileKey,
551
- pngNodes.map((n) => n.nodeId),
552
- "png",
553
- { pngScale }
554
- );
555
- const pngTasks = pngNodes.map(
556
- ({
557
- nodeId,
558
- fileName,
559
- needsCropping,
560
- cropTransform,
561
- requiresImageDimensions
562
- }) => {
563
- const imageUrl = pngUrls[nodeId];
564
- if (!imageUrl) {
565
- Logger.log(
566
- `Skipping PNG render with missing URL for nodeId: ${nodeId}`
567
- );
568
- return null;
569
- }
570
- return () => downloadAndProcessImage(
571
- fileName,
572
- resolvedPath,
573
- imageUrl,
574
- needsCropping,
575
- cropTransform,
576
- requiresImageDimensions,
577
- returnBuffer
578
- ).then(
579
- (result) => ({ ...result, nodeId })
580
- );
581
- }
582
- ).filter(
583
- (task) => task !== null
584
- );
585
- if (pngTasks.length > 0) {
586
- downloadPromises.push(
587
- runWithConcurrency(pngTasks, CONCURRENCY_LIMIT)
533
+ if (!imageUrl) {
534
+ return () => {
535
+ Logger.log(
536
+ `Skipping missing image for ${imageRef ? `imageRef: ${imageRef}` : `nodeId: ${nodeId}`}`
588
537
  );
589
- }
590
- }
591
- if (svgNodes.length > 0) {
592
- const svgUrls = await this.getNodeRenderUrls(
593
- fileKey,
594
- svgNodes.map((n) => n.nodeId),
595
- "svg",
596
- { svgOptions }
597
- );
598
- const svgTasks = svgNodes.map(
599
- ({
538
+ return Promise.resolve({
600
539
  nodeId,
601
- fileName,
602
- needsCropping,
603
- cropTransform,
604
- requiresImageDimensions
605
- }) => {
606
- const imageUrl = svgUrls[nodeId];
607
- if (!imageUrl) {
608
- Logger.log(
609
- `Skipping SVG render with missing URL for nodeId: ${nodeId}`
610
- );
611
- return null;
612
- }
613
- return () => downloadAndProcessImage(
614
- fileName,
615
- resolvedPath,
616
- imageUrl,
617
- needsCropping,
618
- cropTransform,
619
- requiresImageDimensions,
620
- returnBuffer
621
- ).then(
622
- (result) => ({ ...result, nodeId })
623
- );
624
- }
625
- ).filter(
626
- (task) => task !== null
627
- );
628
- if (svgTasks.length > 0) {
629
- downloadPromises.push(
630
- runWithConcurrency(svgTasks, CONCURRENCY_LIMIT)
631
- );
632
- }
540
+ originalDimensions: { width: 0, height: 0 },
541
+ finalDimensions: { width: 0, height: 0 },
542
+ wasCropped: false,
543
+ processingLog: ["Skipped: URL not found"]
544
+ });
545
+ };
633
546
  }
634
- }
635
- const results = await Promise.all(downloadPromises);
636
- return results.flat();
547
+ return async () => {
548
+ const result = await downloadAndProcessImage(
549
+ fileName,
550
+ resolvedPath,
551
+ imageUrl,
552
+ needsCropping,
553
+ cropTransform,
554
+ requiresImageDimensions,
555
+ returnBuffer
556
+ );
557
+ return { ...result, nodeId };
558
+ };
559
+ });
560
+ return runWithConcurrency(tasks, CONCURRENCY_LIMIT);
637
561
  }
638
562
  /**
639
563
  * Get raw Figma API response for a file (for use with flexible extractors)
@@ -1594,7 +1518,11 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
1594
1518
  if (imageAssets.length > 0) {
1595
1519
  const imageNodes = imageAssets.map((asset) => ({
1596
1520
  nodeId: asset.id,
1597
- fileName: sanitizeFileName(asset.name) + `.${imageFormat}`
1521
+ imageRef: asset.imageRef,
1522
+ fileName: sanitizeFileName(asset.name) + (asset.filenameSuffix ? `-${asset.filenameSuffix}` : "") + `.${imageFormat}`,
1523
+ needsCropping: asset.needsCropping,
1524
+ cropTransform: asset.cropTransform,
1525
+ requiresImageDimensions: asset.requiresImageDimensions
1598
1526
  }));
1599
1527
  const downloadResults = await figmaService.downloadImages(
1600
1528
  fileKey,
@@ -1829,28 +1757,37 @@ function sanitizeFileName(name) {
1829
1757
  return name.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
1830
1758
  }
1831
1759
  function findImageAssets(nodes, globalVars) {
1832
- const images = [];
1760
+ const assets = [];
1833
1761
  function traverse(node) {
1834
- const isImageAsset = node.type === "IMAGE-SVG" || hasImageFill(node, globalVars);
1835
- if (isImageAsset) {
1836
- images.push(node);
1762
+ if (node.type === "IMAGE-SVG") {
1763
+ assets.push({
1764
+ ...node,
1765
+ id: node.id,
1766
+ name: node.name,
1767
+ type: "render"
1768
+ });
1769
+ } else if (node.fills && typeof node.fills === "string") {
1770
+ const fillData = globalVars?.styles?.[node.fills];
1771
+ if (Array.isArray(fillData)) {
1772
+ const imageFill = fillData.find((f) => f.type === "IMAGE");
1773
+ if (imageFill) {
1774
+ assets.push({
1775
+ ...node,
1776
+ id: node.id,
1777
+ name: node.name,
1778
+ type: "fill",
1779
+ imageRef: imageFill.imageRef,
1780
+ ...imageFill.imageDownloadArguments
1781
+ });
1782
+ }
1783
+ }
1837
1784
  }
1838
1785
  if (node.children && Array.isArray(node.children)) {
1839
1786
  node.children.forEach(traverse);
1840
1787
  }
1841
1788
  }
1842
1789
  nodes.forEach(traverse);
1843
- return images;
1844
- }
1845
- function hasImageFill(node, globalVars) {
1846
- if (!node.fills || typeof node.fills !== "string") {
1847
- return false;
1848
- }
1849
- const fillData = globalVars?.styles?.[node.fills];
1850
- if (!fillData || !Array.isArray(fillData)) {
1851
- return false;
1852
- }
1853
- return fillData.some((fill) => fill?.type === "IMAGE");
1790
+ return assets;
1854
1791
  }
1855
1792
  function enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths = true, localPath) {
1856
1793
  const imageMap = /* @__PURE__ */ new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-metadata-extractor",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
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",