File size: 9,167 Bytes
6f38c76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
"""
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}")