Download playwright_model.py from dina1/figma: direct link, hf CLI and curl.
- Browser
- Download file 12.8 kB
-
https://huggingface.co/spaces/dina1/figma/resolve/main/playwright_model.py
- Command line
-
hf download hf://spaces/dina1/figma/playwright_model.py
-
curl -L -o playwright_model.py https://huggingface.co/spaces/dina1/figma/resolve/main/playwright_model.py
12.8 kB
| import os | |
| from playwright.sync_api import sync_playwright | |
| from PIL import Image | |
| from fpdf import FPDF | |
| OUTPUT_DIR = "static/outputs" | |
| def capture_workflows(public_url: str, pdf_filename: str = "workflow_screens.pdf"): | |
| os.makedirs(OUTPUT_DIR, exist_ok=True) | |
| pdf_path = os.path.join(OUTPUT_DIR, pdf_filename) | |
| with sync_playwright() as p: | |
| browser = p.chromium.launch(headless=True) | |
| page = browser.new_page() | |
| print(f"Opening page: {public_url}") | |
| page.goto(public_url, wait_until="load") | |
| def wait_for_layout_stable(): | |
| page.evaluate(""" | |
| (() => { | |
| let stableCount = 0; | |
| let lastHeight = document.body.scrollHeight; | |
| return new Promise((resolve) => { | |
| const check = setInterval(() => { | |
| const current = document.body.scrollHeight; | |
| if (Math.abs(current - lastHeight) < 1) { | |
| stableCount++; | |
| if (stableCount >= 5) { | |
| clearInterval(check); | |
| setTimeout(resolve, 300); | |
| } | |
| } else { | |
| stableCount = 0; | |
| lastHeight = current; | |
| } | |
| }, 100); | |
| }); | |
| })(); | |
| """) | |
| def fix_layout_dynamically(): | |
| try: | |
| page.evaluate(""" | |
| (() => { | |
| console.log('π§ Fixing layout...'); | |
| // Reset all layout elements | |
| ['.top-bar, header, .app-header', | |
| 'aside, .sidebar, nav', | |
| 'main, .main-content, .content, .page-content'].forEach(selector => { | |
| document.querySelectorAll(selector).forEach(el => { | |
| if (el) { | |
| ['position','top','left','width','height','zIndex','overflow', | |
| 'transition','margin','padding','boxSizing'].forEach(prop => { | |
| el.style[prop] = ''; | |
| }); | |
| } | |
| }); | |
| }); | |
| // Get accurate element dimensions | |
| const getDims = (el) => { | |
| if (!el || !el.isConnected) return {width: 240, height: 60}; | |
| const rect = el.getBoundingClientRect(); | |
| const style = window.getComputedStyle(el); | |
| return { | |
| width: rect.width + (parseFloat(style.marginLeft) || 0) + (parseFloat(style.marginRight) || 0), | |
| height: rect.height + (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0) | |
| }; | |
| }; | |
| // Fix topbar | |
| const topbar = document.querySelector('.top-bar, header, .app-header, .navbar'); | |
| let topOffset = 60; | |
| if (topbar) { | |
| const dims = getDims(topbar); | |
| topOffset = Math.round(dims.height); | |
| Object.assign(topbar.style, { | |
| position: 'fixed', | |
| top: '0', | |
| left: '0', | |
| width: '100vw', | |
| zIndex: '10000', | |
| transition: 'none', | |
| boxSizing: 'border-box' | |
| }); | |
| document.body.style.marginTop = topOffset + 'px'; | |
| console.log('β Topbar height:', topOffset, 'px'); | |
| } | |
| // Fix sidebar | |
| const sidebar = document.querySelector('aside, .sidebar, nav'); | |
| let sidebarWidth = 240; | |
| if (sidebar) { | |
| const dims = getDims(sidebar); | |
| sidebarWidth = Math.round(dims.width); | |
| Object.assign(sidebar.style, { | |
| position: 'fixed', | |
| top: topOffset + 'px', | |
| left: '0', | |
| height: `calc(100vh - ${topOffset}px)`, | |
| width: sidebarWidth + 'px', | |
| zIndex: '9999', | |
| overflow: 'auto', | |
| transition: 'none', | |
| boxSizing: 'border-box' | |
| }); | |
| console.log('β Sidebar width:', sidebarWidth, 'px'); | |
| } | |
| // Fix main content using padding (more reliable than margin) | |
| const content = document.querySelector('main, .main-content, .content, .page-content'); | |
| if (content) { | |
| content.style.paddingLeft = (sidebarWidth + 20) + 'px'; // +20px for breathing room | |
| content.style.position = 'relative'; | |
| content.style.boxSizing = 'border-box'; | |
| console.log('β Content padding-left:', (sidebarWidth + 20), 'px'); | |
| } | |
| // Hide any duplicate fixed elements that might overlap | |
| document.querySelectorAll('.title-bar, .page-title, [class*="header"]').forEach(el => { | |
| if (el && el !== topbar && window.getComputedStyle(el).position === 'fixed') { | |
| el.style.display = 'none'; | |
| } | |
| }); | |
| window.scrollTo(0, 0); | |
| console.log('β Layout fix completed'); | |
| })(); | |
| """) | |
| except Exception as e: | |
| print(f"[WARN] Layout fix failed: {e}") | |
| # Wait for sidebar and stabilize | |
| page.wait_for_selector("aside, .sidebar, nav", timeout=5000) | |
| wait_for_layout_stable() | |
| fix_layout_dynamically() | |
| page.wait_for_timeout(2000) # Extra time for CSS transitions to complete | |
| # [Rest of the code remains the same as previous version] | |
| # ... (JS logic for capturing workflows, screenshot loop, PDF generation) | |
| # JS logic for capturing workflows | |
| js_logic = """ | |
| (function(){ | |
| const menus=[...document.querySelectorAll('.menu-item')]; | |
| const screens=[...document.querySelectorAll('.screen')]; | |
| window.__ordered=[]; | |
| const seen=new Set(); | |
| for(const m of menus){ | |
| let id=m.dataset.screen||m.dataset.target; | |
| if(!id){ | |
| const href=m.getAttribute('href'); | |
| if(href && href.startsWith('#')) id=href.substring(1); | |
| } | |
| const s=screens.find(x=>x.id===id); | |
| if(s && !seen.has(s)){seen.add(s);window.__ordered.push({menu:m,screen:s});} | |
| } | |
| for(const s of screens) | |
| if(!seen.has(s)) | |
| window.__ordered.push({menu:null,screen:s}); | |
| window.__visitedWorkflows=[]; | |
| window.__currentIndex=0; | |
| window.__done=false; | |
| window.__getSubScreens = function(screen){ | |
| const tabs=[...screen.querySelectorAll('.tab, .nav-link, [role="tab"], [data-tab], .sub-tab, .tab-item')]; | |
| const list=[]; | |
| for(const t of tabs){ | |
| const sub=t.textContent?.trim(); | |
| if(sub && !list.includes(sub)) list.push(sub); | |
| } | |
| return list; | |
| }; | |
| window.__captureNext=function(){ | |
| if(window.__done) return false; | |
| if(window.__currentIndex>=window.__ordered.length){window.__done=true;return false;} | |
| const pair=window.__ordered[window.__currentIndex]; | |
| const {menu,screen}=pair; | |
| if(!screen){window.__done=true;return false;} | |
| const wfName=screen.id || screen.getAttribute('data-name') || ('screen_'+window.__currentIndex); | |
| if(window.__visitedWorkflows.includes(wfName)){ | |
| window.__currentIndex++; | |
| return window.__captureNext(); | |
| } | |
| window.__visitedWorkflows.push(wfName); | |
| document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active')); | |
| document.querySelectorAll('.menu-item').forEach(m=>m.classList.remove('active')); | |
| screen.classList.add('active'); | |
| if(menu) menu.classList.add('active'); | |
| screen.scrollIntoView({behavior:'smooth',block:'center'}); | |
| window.__currentIndex++; | |
| const subs = window.__getSubScreens(screen); | |
| return {screenName:wfName, subScreens:subs}; | |
| }; | |
| window.__clickSubScreen = function(name){ | |
| const tabs=[...document.querySelectorAll('.tab, .nav-link, [role="tab"], [data-tab], .sub-tab, .tab-item')]; | |
| const t=tabs.find(x=>x.textContent.trim()===name); | |
| if(t){t.click(); return true;} | |
| return false; | |
| }; | |
| })(); | |
| """ | |
| page.evaluate(js_logic) | |
| page.wait_for_timeout(1000) | |
| screenshots = [] | |
| index = 0 | |
| # Capture all screens | |
| while True: | |
| result = page.evaluate("window.__captureNext()") | |
| if not result: | |
| break | |
| screen_name = result.get("screenName", f"screen_{index}") | |
| sub_screens = result.get("subScreens", []) | |
| screenshot_path = os.path.join(OUTPUT_DIR, f"{screen_name}.png") | |
| print(f"πΈ Capturing main screen: {screen_name}") | |
| wait_for_layout_stable() | |
| fix_layout_dynamically() | |
| page.wait_for_timeout(1000) | |
| page.screenshot(path=screenshot_path, full_page=True) | |
| screenshots.append(screenshot_path) | |
| # Capture sub-tabs | |
| first_active_skipped = False | |
| for sub in sub_screens: | |
| is_active = page.evaluate( | |
| """(subText) => { | |
| const tabs=[...document.querySelectorAll('.tab, .nav-link, [role="tab"], [data-tab], .sub-tab, .tab-item')]; | |
| const t=tabs.find(x=>x.textContent.trim()===subText); | |
| if(!t) return false; | |
| const cls=t.getAttribute('class')||''; | |
| return cls.includes('active'); | |
| }""", | |
| sub | |
| ) | |
| if is_active and not first_active_skipped: | |
| first_active_skipped = True | |
| continue | |
| print(f" β³ Capturing sub-screen: {sub}") | |
| page.evaluate(f"window.__clickSubScreen('{sub}')") | |
| wait_for_layout_stable() | |
| fix_layout_dynamically() | |
| page.wait_for_timeout(1000) | |
| sub_name_clean = sub.replace(" ", "_").lower() | |
| sub_path = os.path.join(OUTPUT_DIR, f"{screen_name}_{sub_name_clean}.png") | |
| page.screenshot(path=sub_path, full_page=True) | |
| screenshots.append(sub_path) | |
| index += 1 | |
| browser.close() | |
| # Generate PDF | |
| if not screenshots: | |
| raise RuntimeError("No screenshots captured β check if .screen elements exist!") | |
| print(f"π§Ύ Combining {len(screenshots)} screenshots into PDF: {pdf_path}") | |
| pdf = FPDF() | |
| for img_path in screenshots: | |
| image = Image.open(img_path) | |
| w, h = image.size | |
| pdf_w, pdf_h = 210, 297 | |
| aspect = h / w | |
| pdf.add_page() | |
| pdf.image(img_path, 0, 0, pdf_w, pdf_w * aspect) | |
| pdf.output(pdf_path, "F") | |
| print(f"β PDF generated successfully: {pdf_path}") | |
| return pdf_path | |
| generate_ui_report = capture_workflows |