ui-regression-testing-2 / utils /figma_client.py
riazmo's picture
Upload 61 files
6f38c76 verified
"""
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 <file_key> <frame_name> [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}")