""" Server-side export of rendered dependency trees. The trees are drawn by conllu.js and brat in a browser. To return the same SVG/PNG files as the UI download buttons, this module loads the visualizer document in headless Chromium (Playwright) and calls its export functions. """ import base64 import subprocess import sys import threading from typing import Dict from visualizer.template import build_visualizer_document RENDER_TIMEOUT_MS = 45000 _browser_lock = threading.Lock() _browser_installed = False class TreeExportError(RuntimeError): pass def _ensure_chromium() -> None: """Installs Playwright's Chromium once per process if it is missing.""" global _browser_installed if _browser_installed: return subprocess.run( [sys.executable, "-m", "playwright", "install", "chromium"], check=True, capture_output=True, ) _browser_installed = True def export_tree_images(conllu_text: str, title: str = "", png_scale: int = 2) -> Dict[str, bytes]: """ Renders CoNLL-U in headless Chromium and returns {"svg": bytes, "png": bytes}. Calls are serialized: Playwright's sync API is not shared across threads. """ if not conllu_text or not conllu_text.strip(): raise TreeExportError("Empty CoNLL-U input.") from playwright.sync_api import sync_playwright, Error as PlaywrightError document = build_visualizer_document(conllu_text, title=title) with _browser_lock: try: _ensure_chromium() with sync_playwright() as playwright: browser = playwright.chromium.launch() try: page = browser.new_page(viewport={"width": 1600, "height": 900}) page.set_content(document, wait_until="load") page.wait_for_function( "() => ['ready', 'error'].includes(window.visualizerDiagnostics?.stage)", timeout=RENDER_TIMEOUT_MS, ) diagnostics = page.evaluate( "() => ({stage: visualizerDiagnostics.stage, error: visualizerDiagnostics.error || ''})" ) if diagnostics["stage"] != "ready": raise TreeExportError(diagnostics["error"] or "Visualization failed.") svg = page.evaluate("() => getPreparedSVG().xml") png_url = page.evaluate("scale => exportPNGDataURL(scale)", png_scale) finally: browser.close() except PlaywrightError as error: raise TreeExportError(f"Headless rendering failed: {error}") from error except subprocess.CalledProcessError as error: raise TreeExportError("Could not install the headless browser.") from error return { "svg": svg.encode("utf-8"), "png": base64.b64decode(png_url.split(",", 1)[1]), }