""" Scribus Bridge Runner — runs inside the Scribus Python scripting environment. Listens on a Unix socket for JSON commands from the Flask bridge server and dispatches them to the Scribus Python API. Launched via: scribus --python-script scribus_runner.py """ import json import os import socket import sys import threading import traceback from pathlib import Path try: import scribus except ImportError: # Running outside Scribus for testing scribus = None print("[runner] WARNING: scribus module not available (running outside Scribus?)") SOCKET_PATH = "/tmp/scribus_bridge.sock" DESIGNS_DIR = Path("/data/designs") SCREENSHOT_DIR = Path("/tmp/scribus_screenshots") def _ensure_dirs(): DESIGNS_DIR.mkdir(parents=True, exist_ok=True) SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True) # ── Command handlers ── def cmd_new_document(args: dict) -> dict: """Create a new Scribus document.""" width = args.get("width", 210) # mm, A4 default height = args.get("height", 297) margins = args.get("margins", 10) pages = args.get("pages", 1) unit = args.get("unit", 0) # 0=points, 1=mm, 2=inches, 3=picas if scribus: # newDocument(width, height, topMargin, leftMargin, rightMargin, bottomMargin, ..., unit, pages, ...) scribus.newDocument( (width, height), (margins, margins, margins, margins), scribus.PORTRAIT, pages, unit, scribus.FACINGPAGES, scribus.FIRSTPAGELEFT, 1 ) return {"ok": True, "message": f"Created {width}x{height}mm document with {pages} page(s)"} def cmd_add_text_frame(args: dict) -> dict: """Create a text frame and optionally set its content.""" x = args.get("x", 10) y = args.get("y", 10) w = args.get("width", 100) h = args.get("height", 30) text = args.get("text", "") font_size = args.get("fontSize", 12) font_name = args.get("fontName", "Liberation Sans") name = args.get("name") if scribus: frame = scribus.createText(x, y, w, h, name or "") if text: scribus.setText(text, frame) scribus.setFontSize(font_size, frame) try: scribus.setFont(font_name, frame) except Exception: scribus.setFont("Liberation Sans", frame) return {"ok": True, "frame": frame} frame_name = name or f"text_{x}_{y}" return {"ok": True, "frame": frame_name, "simulated": True} def cmd_add_image_frame(args: dict) -> dict: """Create an image frame, optionally loading an image from a path or URL.""" x = args.get("x", 10) y = args.get("y", 10) w = args.get("width", 100) h = args.get("height", 100) image_path = args.get("imagePath", "") name = args.get("name") if scribus: frame = scribus.createImage(x, y, w, h, name or "") if image_path and os.path.exists(image_path): scribus.loadImage(image_path, frame) scribus.setScaleImageToFrame(True, True, frame) return {"ok": True, "frame": frame} frame_name = name or f"image_{x}_{y}" return {"ok": True, "frame": frame_name, "simulated": True} def cmd_add_shape(args: dict) -> dict: """Create a geometric shape (rectangle or ellipse).""" shape_type = args.get("shapeType", "rect") x = args.get("x", 10) y = args.get("y", 10) w = args.get("width", 50) h = args.get("height", 50) fill = args.get("fill") name = args.get("name") if scribus: if shape_type == "ellipse": frame = scribus.createEllipse(x, y, w, h, name or "") else: frame = scribus.createRect(x, y, w, h, name or "") if fill: # Define and set fill color color_name = f"fill_{frame}" r, g, b = _parse_color(fill) scribus.defineColorRGB(color_name, r, g, b) scribus.setFillColor(color_name, frame) return {"ok": True, "frame": frame} return {"ok": True, "frame": name or f"{shape_type}_{x}_{y}", "simulated": True} def cmd_get_doc_state(args: dict) -> dict: """Return a full snapshot of the current document state.""" if not scribus: return {"error": "No scribus module", "simulated": True} try: page_count = scribus.pageCount() except Exception: return {"pages": [], "frames": [], "message": "No document open"} pages = [] for p in range(1, page_count + 1): scribus.gotoPage(p) w, h = scribus.getPageSize() pages.append({"number": p, "width": w, "height": h}) frames = [] all_objects = scribus.getAllObjects() for obj_name in all_objects: obj_type = scribus.getObjectType(obj_name) x, y = scribus.getPosition(obj_name) w, h = scribus.getSize(obj_name) frame_info = { "name": obj_name, "type": obj_type, "x": x, "y": y, "width": w, "height": h, } if obj_type == "TextFrame": try: frame_info["text"] = scribus.getText(obj_name) frame_info["fontSize"] = scribus.getFontSize(obj_name) frame_info["fontName"] = scribus.getFont(obj_name) except Exception: pass frames.append(frame_info) return {"pages": pages, "frames": frames} def cmd_screenshot(args: dict) -> dict: """Export the current page as PNG.""" dpi = args.get("dpi", 72) _ensure_dirs() path = str(SCREENSHOT_DIR / "current_page.png") if scribus: try: scribus.savePageAsEPS(path.replace(".png", ".eps")) # Fallback: use scribus PDF export + convert, or direct image export # Scribus 1.5 has limited direct PNG export; use saveDocAs + external convert scribus.saveDocAs(path.replace(".png", ".sla")) return {"ok": True, "path": path, "note": "SLA saved; PNG conversion may require external tool"} except Exception as e: return {"error": f"Screenshot failed: {str(e)}"} return {"ok": True, "path": path, "simulated": True} def cmd_save_as_sla(args: dict) -> dict: """Save the document as .sla file.""" space = args.get("space", "default") filename = args.get("filename", "design.sla") _ensure_dirs() save_dir = DESIGNS_DIR / space save_dir.mkdir(parents=True, exist_ok=True) save_path = str(save_dir / filename) if scribus: scribus.saveDocAs(save_path) return {"ok": True, "path": save_path} return {"ok": True, "path": save_path, "simulated": True} def cmd_move_frame(args: dict) -> dict: """Move a frame by relative or absolute coordinates.""" name = args.get("name", "") x = args.get("x", 0) y = args.get("y", 0) absolute = args.get("absolute", False) if scribus and name: if absolute: scribus.moveObjectAbs(x, y, name) else: scribus.moveObject(x, y, name) return {"ok": True} return {"ok": True, "simulated": True} def cmd_delete_frame(args: dict) -> dict: """Delete a frame by name.""" name = args.get("name", "") if scribus and name: scribus.deleteObject(name) return {"ok": True} return {"ok": True, "simulated": True} def cmd_set_background_color(args: dict) -> dict: """Set the page background color.""" color = args.get("color", "#ffffff") if scribus: r, g, b = _parse_color(color) color_name = "page_bg" scribus.defineColorRGB(color_name, r, g, b) # Scribus doesn't have direct page background — create a full-page rect w, h = scribus.getPageSize() bg = scribus.createRect(0, 0, w, h, "background_rect") scribus.setFillColor(color_name, bg) scribus.setLineWidth(0, bg) scribus.sentToLayer("Background", bg) if False else None # Send to back try: for _ in range(50): scribus.moveSelectionToBack() except Exception: pass return {"ok": True, "frame": bg} return {"ok": True, "simulated": True} # ── Helpers ── def _parse_color(color_str: str) -> tuple: """Parse hex color string to (r, g, b) tuple.""" color_str = color_str.lstrip("#") if len(color_str) == 6: return (int(color_str[0:2], 16), int(color_str[2:4], 16), int(color_str[4:6], 16)) return (0, 0, 0) COMMAND_MAP = { "new_document": cmd_new_document, "add_text_frame": cmd_add_text_frame, "add_image_frame": cmd_add_image_frame, "add_shape": cmd_add_shape, "get_doc_state": cmd_get_doc_state, "screenshot": cmd_screenshot, "save_as_sla": cmd_save_as_sla, "move_frame": cmd_move_frame, "delete_frame": cmd_delete_frame, "set_background_color": cmd_set_background_color, } def handle_command(data: dict) -> dict: """Dispatch a command to the appropriate handler.""" action = data.get("action", "") args = data.get("args", {}) handler = COMMAND_MAP.get(action) if not handler: return {"error": f"Unknown action: {action}", "available": list(COMMAND_MAP.keys())} try: return handler(args) except Exception as e: return {"error": f"Command '{action}' failed: {str(e)}", "traceback": traceback.format_exc()} def run_socket_server(): """Listen on Unix socket for commands from the Flask bridge.""" if os.path.exists(SOCKET_PATH): os.remove(SOCKET_PATH) server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) server.bind(SOCKET_PATH) os.chmod(SOCKET_PATH, 0o666) server.listen(5) print(f"[runner] Listening on {SOCKET_PATH}") while True: try: conn, _ = server.accept() data = b"" while True: chunk = conn.recv(4096) if not chunk: break data += chunk if b"\n" in data: break if data: cmd = json.loads(data.decode("utf-8").strip()) result = handle_command(cmd) response = json.dumps(result) + "\n" conn.sendall(response.encode("utf-8")) conn.close() except Exception as e: print(f"[runner] Socket error: {e}", file=sys.stderr) if __name__ == "__main__": _ensure_dirs() print("[runner] Scribus bridge runner starting...") # Run socket server in a thread so Scribus event loop can continue t = threading.Thread(target=run_socket_server, daemon=True) t.start() print("[runner] Socket server thread started") # Keep the script alive # When run via --python-script, Scribus will execute this then exit # We need to keep it running for the socket server try: while True: import time time.sleep(1) except KeyboardInterrupt: print("[runner] Shutting down")