AkashKumarave commited on
Commit
045423f
·
verified ·
1 Parent(s): 324c60c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +417 -151
app.py CHANGED
@@ -1,179 +1,445 @@
1
 
2
- import gradio as gr
 
3
  import requests
 
 
4
  from bs4 import BeautifulSoup
5
- import json
 
 
 
 
6
  import base64
 
7
  from PIL import Image
8
- import io
9
- import os
10
- from urllib.parse import urlparse
11
- import re
12
 
13
- def clean_url(url):
14
- """Ensure URL has proper protocol"""
15
- if not url.startswith(('http://', 'https://')):
16
- url = 'https://' + url
17
- return url
18
 
19
- def get_page_screenshot(url):
20
- """Get screenshot of the webpage using a screenshot API"""
21
- try:
22
- # Using a free screenshot API (limited, for demo purposes)
23
- api_url = f"https://api.apiflash.com/v1/urltoimage?access_key=demo&url={url}&format=jpeg&quality=80"
24
- response = requests.get(api_url)
25
- if response.status_code == 200:
26
- return response.content
27
- else:
28
- return None
29
- except Exception as e:
30
- print(f"Screenshot error: {str(e)}")
31
- return None
32
-
33
- def extract_styles(element):
34
- """Extract CSS styles from an element"""
35
- styles = {}
36
- if element.has_attr('style'):
37
- style_text = element.get('style')
38
- style_pairs = [s.strip() for s in style_text.split(';') if s.strip()]
39
- for pair in style_pairs:
40
- if ':' in pair:
41
- prop, val = pair.split(':', 1)
42
- styles[prop.strip()] = val.strip()
43
- return styles
44
 
45
- def extract_colors(styles):
46
- """Extract colors from CSS styles"""
47
- colors = []
48
- color_props = ['color', 'background-color', 'border-color']
49
- for prop in color_props:
50
- if prop in styles and styles[prop] not in ['transparent', 'inherit', 'initial']:
51
- colors.append(styles[prop])
52
- return colors
 
53
 
54
- def extract_fonts(styles):
55
- """Extract fonts from CSS styles"""
56
- fonts = []
57
- if 'font-family' in styles:
58
- font_str = styles['font-family']
59
- # Split by comma and clean up quotes
60
- font_list = [f.strip().strip('"\'') for f in font_str.split(',')]
61
- fonts.extend(font_list)
62
- return fonts
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
- def process_website(url):
65
- """Process a website and extract design elements"""
66
- url = clean_url(url)
67
-
68
  try:
69
- # Fetch the webpage
70
- headers = {
71
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
72
- }
73
- response = requests.get(url, headers=headers, timeout=15)
74
- if response.status_code != 200:
75
- return {"error": f"Failed to fetch website (Status code: {response.status_code})"}
76
-
77
- # Parse HTML
78
- soup = BeautifulSoup(response.text, 'html.parser')
79
 
80
- # Get screenshot
81
- screenshot = get_page_screenshot(url)
82
- screenshot_base64 = None
83
- if screenshot:
84
- screenshot_base64 = base64.b64encode(screenshot).decode('utf-8')
 
 
 
 
 
85
 
86
- # Extract text content
87
- text_elements = []
88
- for text_tag in soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'a', 'button']):
89
- if text_tag.text.strip():
90
- text_elements.append({
91
- "tag": text_tag.name,
92
- "text": text_tag.text.strip(),
93
- "styles": extract_styles(text_tag)
94
- })
95
 
96
- # Extract colors
97
- all_colors = []
98
- for tag in soup.find_all(True):
99
- styles = extract_styles(tag)
100
- colors = extract_colors(styles)
101
- all_colors.extend(colors)
102
 
103
- # Extract fonts
104
- all_fonts = []
105
- for tag in soup.find_all(True):
106
- styles = extract_styles(tag)
107
- fonts = extract_fonts(styles)
108
- all_fonts.extend(fonts)
109
 
110
- # Extract images
111
- images = []
112
- for img in soup.find_all('img'):
113
- if img.has_attr('src'):
114
- src = img['src']
115
- if src.startswith('//'):
116
- src = 'https:' + src
117
- elif not src.startswith(('http://', 'https://')):
118
- # Handle relative URLs
119
- base_url = '{uri.scheme}://{uri.netloc}'.format(uri=urlparse(url))
120
- if src.startswith('/'):
121
- src = base_url + src
122
- else:
123
- src = base_url + '/' + src
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
- images.append({
126
- "src": src,
127
- "alt": img.get('alt', ''),
128
- "width": img.get('width', ''),
129
- "height": img.get('height', '')
130
- })
131
-
132
- # Create result
133
- result = {
134
- "url": url,
135
- "title": soup.title.string if soup.title else "",
136
- "screenshot": screenshot_base64,
137
- "text_elements": text_elements[:50], # Limit to 50 elements
138
- "colors": list(set(all_colors))[:20], # Remove duplicates and limit
139
- "fonts": list(set(all_fonts))[:10], # Remove duplicates and limit
140
- "images": images[:20] # Limit to 20 images
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  }
142
 
143
- return result
 
 
 
 
144
 
145
- except Exception as e:
146
- return {"error": str(e)}
147
-
148
- def figma_conversion_api(url):
149
- """API endpoint for Figma plugin to convert websites"""
150
- if not url:
151
- return {"error": "No URL provided"}
152
 
153
- result = process_website(url)
154
- return result
155
 
156
- # Create Gradio interface
157
- with gr.Blocks() as demo:
158
- gr.Markdown("# Website to Figma Converter API")
159
 
160
- with gr.Tab("API Endpoint"):
161
- gr.Markdown("""
162
- ## API Usage
163
- This API provides website conversion functionality for the Figma plugin.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
- ### Example usage:
166
- Send a POST request with a URL parameter to convert a website to Figma-compatible format.
167
- """)
 
 
168
 
169
- with gr.Tab("Test Interface"):
170
- with gr.Row():
171
- url_input = gr.Textbox(label="Enter Website URL", placeholder="https://example.com")
172
- convert_button = gr.Button("Convert Website")
 
 
 
 
 
 
 
173
 
174
- output = gr.JSON(label="Conversion Result")
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
- convert_button.click(figma_conversion_api, inputs=url_input, outputs=output)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
- # Mount the Gradio app
179
- demo.launch()
 
 
1
 
2
+ import os
3
+ import json
4
  import requests
5
+ import traceback
6
+ from flask import Flask, request, jsonify
7
  from bs4 import BeautifulSoup
8
+ from selenium import webdriver
9
+ from selenium.webdriver.chrome.options import Options
10
+ from selenium.webdriver.chrome.service import Service
11
+ import time
12
+ import re
13
  import base64
14
+ import logging
15
  from PIL import Image
16
+ from io import BytesIO
17
+ import cssutils
18
+ import urllib.parse
 
19
 
20
+ app = Flask(__name__)
 
 
 
 
21
 
22
+ # Setup logging
23
+ logging.basicConfig(level=logging.INFO)
24
+ logger = logging.getLogger(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
+ # Configure Chrome options for headless browsing
27
+ def get_chrome_options():
28
+ options = Options()
29
+ options.add_argument("--headless")
30
+ options.add_argument("--no-sandbox")
31
+ options.add_argument("--disable-dev-shm-usage")
32
+ options.add_argument("--disable-gpu")
33
+ options.add_argument("--window-size=1440,900")
34
+ return options
35
 
36
+ @app.route('/')
37
+ def home():
38
+ return '''
39
+ <html>
40
+ <head>
41
+ <title>Website to Figma API</title>
42
+ <style>
43
+ body {
44
+ font-family: Arial, sans-serif;
45
+ max-width: 800px;
46
+ margin: 0 auto;
47
+ padding: 20px;
48
+ line-height: 1.6;
49
+ }
50
+ h1 {
51
+ color: #333;
52
+ }
53
+ pre {
54
+ background-color: #f5f5f5;
55
+ padding: 10px;
56
+ border-radius: 5px;
57
+ overflow-x: auto;
58
+ }
59
+ </style>
60
+ </head>
61
+ <body>
62
+ <h1>Website to Figma Converter API</h1>
63
+ <p>This API converts websites into Figma-compatible data structures.</p>
64
+
65
+ <h2>API Endpoints:</h2>
66
+ <h3>POST /api/convert</h3>
67
+ <p>Converts a website to Figma elements.</p>
68
+
69
+ <h4>Request Body:</h4>
70
+ <pre>
71
+ {
72
+ "url": "https://example.com",
73
+ "viewport_width": 1440 // Optional, defaults to 1440px
74
+ }
75
+ </pre>
76
+
77
+ <h4>Response:</h4>
78
+ <pre>
79
+ {
80
+ "status": "success",
81
+ "viewport_width": 1440,
82
+ "viewport_height": 900,
83
+ "elements": [
84
+ {
85
+ "type": "rectangle",
86
+ "x": 0,
87
+ "y": 0,
88
+ "width": 200,
89
+ "height": 100,
90
+ "style": {
91
+ "backgroundColor": "#ffffff",
92
+ "borderRadius": "5px"
93
+ }
94
+ },
95
+ // more elements...
96
+ ]
97
+ }
98
+ </pre>
99
+
100
+ <p>This API is part of the Website to Figma plugin.</p>
101
+ </body>
102
+ </html>
103
+ '''
104
 
105
+ @app.route('/api/convert', methods=['POST'])
106
+ def convert_website():
 
 
107
  try:
108
+ data = request.json
109
+ if not data:
110
+ return jsonify({"error": "No data provided"}), 400
 
 
 
 
 
 
 
111
 
112
+ url = data.get('url')
113
+ if not url:
114
+ return jsonify({"error": "URL is required"}), 400
115
+
116
+ # Add http if not present
117
+ if not url.startswith('http'):
118
+ url = 'https://' + url
119
+
120
+ viewport_width = int(data.get('viewport_width', 1440))
121
+ viewport_height = 900 # Default height
122
 
123
+ logger.info(f"Converting website: {url} with viewport width: {viewport_width}")
 
 
 
 
 
 
 
 
124
 
125
+ # Set up Chrome driver
126
+ options = get_chrome_options()
127
+ options.add_argument(f"--window-size={viewport_width},{viewport_height}")
 
 
 
128
 
129
+ driver = webdriver.Chrome(options=options)
130
+ driver.set_window_size(viewport_width, viewport_height)
 
 
 
 
131
 
132
+ try:
133
+ # Load the page
134
+ driver.get(url)
135
+
136
+ # Wait for page to load (adjust as needed)
137
+ time.sleep(5)
138
+
139
+ # Get page dimensions
140
+ page_dimensions = driver.execute_script("""
141
+ return {
142
+ width: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth),
143
+ height: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)
144
+ };
145
+ """)
146
+
147
+ viewport_height = page_dimensions['height']
148
+
149
+ # Parse the page elements
150
+ elements = extract_elements(driver)
151
+
152
+ # Get all CSS styles
153
+ styles = extract_styles(driver)
154
+
155
+ # Apply styles to elements
156
+ apply_styles_to_elements(elements, styles)
157
+
158
+ # Prepare response
159
+ response = {
160
+ "status": "success",
161
+ "url": url,
162
+ "viewport_width": viewport_width,
163
+ "viewport_height": viewport_height,
164
+ "elements": elements
165
+ }
166
+
167
+ return jsonify(response)
168
+
169
+ finally:
170
+ driver.quit()
171
+
172
+ except Exception as e:
173
+ logger.error(f"Error: {str(e)}")
174
+ logger.error(traceback.format_exc())
175
+ return jsonify({"error": str(e), "traceback": traceback.format_exc()}), 500
176
+
177
+ def extract_elements(driver):
178
+ """Extract elements from the webpage"""
179
+ elements = []
180
+
181
+ # Execute JavaScript to extract elements
182
+ elements_data = driver.execute_script("""
183
+ function getElementInfo(element, depth = 0) {
184
+ if (!element || depth > 15) return null;
185
+
186
+ // Skip invisible elements
187
+ if (element.offsetWidth === 0 || element.offsetHeight === 0) return null;
188
+
189
+ // Get element position and size
190
+ const rect = element.getBoundingClientRect();
191
+ if (rect.width < 1 || rect.height < 1) return null;
192
+
193
+ // Create element data
194
+ const elementData = {
195
+ tagName: element.tagName.toLowerCase(),
196
+ id: element.id || null,
197
+ className: element.className || null,
198
+ x: rect.left,
199
+ y: rect.top,
200
+ width: rect.width,
201
+ height: rect.height
202
+ };
203
+
204
+ // Handle text elements
205
+ if (element.tagName.toLowerCase() === 'p' ||
206
+ element.tagName.toLowerCase() === 'h1' ||
207
+ element.tagName.toLowerCase() === 'h2' ||
208
+ element.tagName.toLowerCase() === 'h3' ||
209
+ element.tagName.toLowerCase() === 'h4' ||
210
+ element.tagName.toLowerCase() === 'h5' ||
211
+ element.tagName.toLowerCase() === 'h6' ||
212
+ element.tagName.toLowerCase() === 'span' ||
213
+ element.tagName.toLowerCase() === 'a') {
214
 
215
+ const textContent = element.textContent.trim();
216
+ if (textContent) {
217
+ elementData.type = 'text';
218
+ elementData.content = textContent;
219
+
220
+ // Get computed style
221
+ const style = window.getComputedStyle(element);
222
+ elementData.style = {
223
+ color: style.color,
224
+ fontSize: style.fontSize,
225
+ fontWeight: style.fontWeight,
226
+ lineHeight: style.lineHeight,
227
+ textAlign: style.textAlign,
228
+ fontFamily: style.fontFamily
229
+ };
230
+ }
231
+ }
232
+
233
+ // Handle image elements
234
+ else if (element.tagName.toLowerCase() === 'img') {
235
+ elementData.type = 'image';
236
+ elementData.src = element.src;
237
+ elementData.alt = element.alt || '';
238
+ }
239
+
240
+ // Handle div/container elements
241
+ else if (element.tagName.toLowerCase() === 'div' ||
242
+ element.tagName.toLowerCase() === 'section' ||
243
+ element.tagName.toLowerCase() === 'article' ||
244
+ element.tagName.toLowerCase() === 'header' ||
245
+ element.tagName.toLowerCase() === 'footer' ||
246
+ element.tagName.toLowerCase() === 'main') {
247
+
248
+ elementData.type = 'div';
249
+
250
+ // Get background color
251
+ const style = window.getComputedStyle(element);
252
+ elementData.style = {
253
+ backgroundColor: style.backgroundColor,
254
+ borderRadius: style.borderRadius,
255
+ borderWidth: style.borderWidth,
256
+ borderColor: style.borderColor,
257
+ boxShadow: style.boxShadow
258
+ };
259
+
260
+ // Check if there are direct text children
261
+ if (element.childNodes.length > 0) {
262
+ let directText = '';
263
+ for (let i = 0; i < element.childNodes.length; i++) {
264
+ if (element.childNodes[i].nodeType === Node.TEXT_NODE) {
265
+ const text = element.childNodes[i].textContent.trim();
266
+ if (text) directText += text + ' ';
267
+ }
268
+ }
269
+
270
+ if (directText.trim()) {
271
+ elementData.type = 'text';
272
+ elementData.content = directText.trim();
273
+ elementData.style.color = style.color;
274
+ elementData.style.fontSize = style.fontSize;
275
+ elementData.style.fontWeight = style.fontWeight;
276
+ }
277
+ }
278
+
279
+ // Get children
280
+ const children = [];
281
+ for (let i = 0; i < element.children.length; i++) {
282
+ const childData = getElementInfo(element.children[i], depth + 1);
283
+ if (childData) children.push(childData);
284
+ }
285
+
286
+ if (children.length > 0) {
287
+ elementData.children = children;
288
+ elementData.type = 'container';
289
+ }
290
+ }
291
+
292
+ // Handle button elements
293
+ else if (element.tagName.toLowerCase() === 'button' ||
294
+ (element.tagName.toLowerCase() === 'a' && window.getComputedStyle(element).display === 'inline-block')) {
295
+
296
+ elementData.type = 'rectangle';
297
+ elementData.name = 'Button';
298
+
299
+ // Get style
300
+ const style = window.getComputedStyle(element);
301
+ elementData.style = {
302
+ backgroundColor: style.backgroundColor,
303
+ color: style.color,
304
+ borderRadius: style.borderRadius,
305
+ borderWidth: style.borderWidth,
306
+ borderColor: style.borderColor
307
+ };
308
+
309
+ // Add text content
310
+ const textContent = element.textContent.trim();
311
+ if (textContent) {
312
+ elementData.content = textContent;
313
+ }
314
+ }
315
+
316
+ else {
317
+ // Default to rectangle for other elements
318
+ elementData.type = 'rectangle';
319
+
320
+ // Get style
321
+ const style = window.getComputedStyle(element);
322
+ elementData.style = {
323
+ backgroundColor: style.backgroundColor
324
+ };
325
+
326
+ // Get children
327
+ const children = [];
328
+ for (let i = 0; i < element.children.length; i++) {
329
+ const childData = getElementInfo(element.children[i], depth + 1);
330
+ if (childData) children.push(childData);
331
+ }
332
+
333
+ if (children.length > 0) {
334
+ elementData.children = children;
335
+ }
336
+ }
337
+
338
+ return elementData;
339
  }
340
 
341
+ function getVisibleElements() {
342
+ const bodyElement = document.body;
343
+ const result = getElementInfo(bodyElement);
344
+ return result ? result.children || [] : [];
345
+ }
346
 
347
+ return getVisibleElements();
348
+ """)
 
 
 
 
 
349
 
350
+ return elements_data
 
351
 
352
+ def extract_styles(driver):
353
+ """Extract CSS styles from the webpage"""
354
+ styles = {}
355
 
356
+ # Execute JavaScript to extract styles
357
+ css_rules = driver.execute_script("""
358
+ const sheets = document.styleSheets;
359
+ const rules = [];
360
+
361
+ for (let i = 0; i < sheets.length; i++) {
362
+ try {
363
+ const sheet = sheets[i];
364
+ const ruleList = sheet.rules || sheet.cssRules;
365
+
366
+ for (let j = 0; j < ruleList.length; j++) {
367
+ try {
368
+ const rule = ruleList[j];
369
+ if (rule.selectorText) {
370
+ rules.push({
371
+ selector: rule.selectorText,
372
+ style: rule.style.cssText
373
+ });
374
+ }
375
+ } catch (e) {
376
+ // Skip rule if it can't be accessed
377
+ }
378
+ }
379
+ } catch (e) {
380
+ // Skip stylesheet if it can't be accessed
381
+ }
382
+ }
383
 
384
+ return rules;
385
+ """)
386
+
387
+ for rule in css_rules:
388
+ styles[rule['selector']] = rule['style']
389
 
390
+ return styles
391
+
392
+ def apply_styles_to_elements(elements, styles):
393
+ """Apply CSS styles to elements"""
394
+ for element in elements:
395
+ if 'className' in element and element['className']:
396
+ class_names = element['className'].split()
397
+ for class_name in class_names:
398
+ selector = '.' + class_name
399
+ if selector in styles:
400
+ apply_style(element, styles[selector])
401
 
402
+ if 'id' in element and element['id']:
403
+ selector = '#' + element['id']
404
+ if selector in styles:
405
+ apply_style(element, styles[selector])
406
+
407
+ # Apply styles to children recursively
408
+ if 'children' in element and element['children']:
409
+ apply_styles_to_elements(element['children'], styles)
410
+
411
+ def apply_style(element, style_text):
412
+ """Apply a CSS style to an element"""
413
+ if 'style' not in element:
414
+ element['style'] = {}
415
 
416
+ # Parse CSS text to extract properties
417
+ style_dict = parse_css_text(style_text)
418
+
419
+ # Apply properties to element style
420
+ for key, value in style_dict.items():
421
+ if key not in element['style'] or not element['style'][key]:
422
+ element['style'][key] = value
423
+
424
+ def parse_css_text(css_text):
425
+ """Parse CSS text into a dictionary"""
426
+ style_dict = {}
427
+
428
+ # Basic parsing of CSS text
429
+ for item in css_text.split(';'):
430
+ if ':' in item:
431
+ key, value = item.split(':', 1)
432
+ key = key.strip()
433
+ value = value.strip()
434
+ if key and value:
435
+ # Convert to camelCase for JavaScript
436
+ key_parts = key.split('-')
437
+ if len(key_parts) > 1:
438
+ key = key_parts[0] + ''.join(part.capitalize() for part in key_parts[1:])
439
+ style_dict[key] = value
440
+
441
+ return style_dict
442
 
443
+ if __name__ == "__main__":
444
+ port = int(os.environ.get("PORT", 7860))
445
+ app.run(host="0.0.0.0", port=port)