""" Figma Client - Export frames and extract design specifications Uses Figma REST API to export frames as PNG images FIXED: Using the correct /images endpoint that preserves frame dimensions """ import os import requests import logging from typing import Dict, Any, Optional import time logger = logging.getLogger(__name__) class FigmaClient: """Client for interacting with Figma API.""" def __init__(self, access_token: str = None): """ Initialize Figma Client. Args: access_token: Figma API access token """ self.access_token = access_token or os.getenv("FIGMA_ACCESS_TOKEN", "") self.base_url = "https://api.figma.com/v1" self.headers = { "X-Figma-Token": self.access_token } def _make_request(self, method: str, url: str, params: Dict = None, timeout: int = 30) -> requests.Response: """ Make HTTP request with retry logic. Args: method: HTTP method (GET, POST, etc.) url: Request URL params: Query parameters timeout: Request timeout in seconds Returns: Response object """ max_retries = 3 retry_delay = 1 for attempt in range(max_retries): try: if method == "GET": response = requests.get(url, headers=self.headers, params=params, timeout=timeout) else: raise ValueError(f"Unsupported method: {method}") response.raise_for_status() return response except requests.exceptions.RequestException as e: if attempt < max_retries - 1: print(f" ⚠️ Request failed: {str(e)}. Retrying in {retry_delay}s...") time.sleep(retry_delay) retry_delay *= 2 else: raise def get_file_structure(self, file_key: str) -> Dict[str, Any]: """ Get the file structure from Figma. Args: file_key: Figma file key Returns: File structure data """ url = f"{self.base_url}/files/{file_key}" response = self._make_request("GET", url) return response.json() def find_frames(self, file_key: str) -> Dict[str, Dict[str, Any]]: """ Find all top-level frames in the Figma file. Args: file_key: Figma file key Returns: Dictionary mapping frame names to frame data (id, width, height) """ file_data = self.get_file_structure(file_key) frames = {} def traverse_nodes(node, depth=0): """Recursively traverse nodes to find frames.""" # Only look at direct children of canvases (depth 1) # This avoids nested components if node.get("type") == "FRAME" and depth == 1: frames[node.get("name")] = { "id": node.get("id"), "width": node.get("absoluteBoundingBox", {}).get("width"), "height": node.get("absoluteBoundingBox", {}).get("height") } # Only traverse children if we're at canvas level (depth 0) if node.get("type") == "CANVAS" and depth == 0: if "children" in node: for child in node["children"]: traverse_nodes(child, depth + 1) # Start traversal from document root if "document" in file_data: for child in file_data["document"].get("children", []): traverse_nodes(child, depth=0) return frames def export_frame( self, file_key: str, frame_name: str, output_path: str, scale: float = 2.0, format: str = "png" ) -> str: """ Export a frame from Figma as an image. Uses the correct /images endpoint that preserves frame dimensions. Args: file_key: Figma file key frame_name: Name of the frame to export output_path: Path to save the exported image scale: Export scale (1.0 = 100%, 2.0 = 200%) format: Export format (png, jpg, svg, pdf) Returns: Path to the exported image """ # Get frame ID frames = self.find_frames(file_key) if frame_name not in frames: available = list(frames.keys()) raise ValueError(f"Frame '{frame_name}' not found. Available frames: {available}") frame_id = frames[frame_name]["id"] frame_width = frames[frame_name]["width"] frame_height = frames[frame_name]["height"] print(f" 📥 Exporting frame: {frame_name}") print(f" Frame ID: {frame_id}") print(f" Dimensions: {frame_width}x{frame_height}") # Use CORRECT endpoint: /images/{file_key} (NOT /files/{file_key}/images) # This endpoint returns images with node_id as the key, preserving frame dimensions url = f"{self.base_url}/images/{file_key}" params = { "ids": frame_id, "format": format, "scale": scale } response = self._make_request("GET", url, params=params) image_data = response.json() # Check for errors if image_data.get("error"): raise ValueError(f"Figma API error: {image_data.get('error')}") # Parse the response - images are directly under 'images' key with node_id as key images = image_data.get("images", {}) if not images: raise ValueError(f"Failed to export frame '{frame_name}' - no image URLs returned") # The correct endpoint returns the node_id as the key if frame_id in images: image_url = images[frame_id] else: # Fallback: use first available image image_url = list(images.values())[0] if not image_url: raise ValueError(f"Failed to export frame '{frame_name}' - image URL is empty") # Download the image image_response = requests.get(image_url, timeout=30) image_response.raise_for_status() # Save to file os.makedirs(os.path.dirname(output_path), exist_ok=True) with open(output_path, "wb") as f: f.write(image_response.content) logger.info(f"Exported frame '{frame_name}' to {output_path}") print(f" ✓ Exported: {output_path}") return output_path def export_frames_by_pattern( self, file_key: str, pattern: str, output_dir: str, scale: float = 2.0, format: str = "png" ) -> Dict[str, str]: """ Export multiple frames matching a pattern. Args: file_key: Figma file key pattern: Frame name pattern (e.g., "Checkout-*") output_dir: Directory to save exported frames scale: Export scale format: Export format Returns: Dictionary mapping frame names to export paths """ frames = self.find_frames(file_key) exported = {} for frame_name in frames.keys(): if pattern.replace("*", "") in frame_name: output_path = os.path.join(output_dir, f"{frame_name}.{format}") try: self.export_frame(file_key, frame_name, output_path, scale, format) exported[frame_name] = output_path except Exception as e: logger.error(f"Failed to export frame '{frame_name}': {str(e)}") return exported def get_design_specs(self, file_key: str) -> Dict[str, Any]: """ Extract design specifications from Figma file. Args: file_key: Figma file key Returns: Dictionary with design specifications """ file_data = self.get_file_structure(file_key) frames = self.find_frames(file_key) specs = { "file_name": file_data.get("name", ""), "frames": frames, "colors": [], "typography": [] } return specs if __name__ == "__main__": # Test the client import sys client = FigmaClient() if len(sys.argv) < 3: print("Usage: python figma_client.py [output_path]") sys.exit(1) file_key = sys.argv[1] frame_name = sys.argv[2] output_path = sys.argv[3] if len(sys.argv) > 3 else f"{frame_name}.png" print(f"Exporting frame: {frame_name}") result = client.export_frame(file_key, frame_name, output_path) print(f"Saved to: {result}")