""" Scribus Bridge Server — HTTP API for controlling Scribus from rSpace. Architecture: rspace container → HTTP → this Flask server (port 8765) │ Unix socket scribus --python-script scribus_runner.py The runner script executes inside the Scribus process (required by Scribus Python API). It listens on a Unix socket for JSON commands. This Flask server translates HTTP requests into socket commands. """ import json import os import socket import time from pathlib import Path from flask import Flask, request, jsonify app = Flask(__name__) BRIDGE_SECRET = os.environ.get("BRIDGE_SECRET", "") SOCKET_PATH = "/tmp/scribus_bridge.sock" DESIGNS_DIR = Path("/data/designs") def _check_auth(): """Verify X-Bridge-Secret header.""" if not BRIDGE_SECRET: return None # No secret configured, allow all token = request.headers.get("X-Bridge-Secret", "") if token != BRIDGE_SECRET: return jsonify({"error": "Unauthorized"}), 401 return None def _send_command(cmd: dict, timeout: float = 30.0) -> dict: """Send a JSON command to the Scribus runner via Unix socket.""" if not os.path.exists(SOCKET_PATH): return {"error": "Scribus runner not connected. Call /api/scribus/start first."} try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(timeout) sock.connect(SOCKET_PATH) payload = json.dumps(cmd) + "\n" sock.sendall(payload.encode("utf-8")) # Read response (newline-delimited JSON) buf = b"" while True: chunk = sock.recv(4096) if not chunk: break buf += chunk if b"\n" in buf: break sock.close() return json.loads(buf.decode("utf-8").strip()) except socket.timeout: return {"error": "Command timed out"} except ConnectionRefusedError: return {"error": "Scribus runner not responding"} except Exception as e: return {"error": f"Bridge error: {str(e)}"} @app.before_request def before_request(): auth = _check_auth() if auth: return auth @app.route("/health", methods=["GET"]) def health(): runner_alive = os.path.exists(SOCKET_PATH) return jsonify({ "ok": True, "service": "scribus-bridge", "runner_connected": runner_alive, }) @app.route("/api/scribus/start", methods=["POST"]) def start_scribus(): """Verify runner socket is available. Optionally launch Scribus GUI for real rendering.""" # The runner process is managed by supervisor and should already be listening. # Wait briefly for socket if it's still starting up. for _ in range(10): if os.path.exists(SOCKET_PATH): return jsonify({"ok": True, "message": "Runner connected", "runner_connected": True}) time.sleep(0.5) return jsonify({"ok": False, "error": "Runner socket not available. Check supervisor logs."}), 500 @app.route("/api/scribus/command", methods=["POST"]) def scribus_command(): """Execute a Scribus command via the runner.""" body = request.get_json(silent=True) if not body or "action" not in body: return jsonify({"error": "Missing 'action' in request body"}), 400 result = _send_command(body) status = 200 if "error" not in result else 500 return jsonify(result), status @app.route("/api/scribus/state", methods=["GET"]) def scribus_state(): """Return the full document state as JSON.""" result = _send_command({"action": "get_doc_state"}) status = 200 if "error" not in result else 500 return jsonify(result), status @app.route("/api/scribus/screenshot", methods=["GET"]) def scribus_screenshot(): """Export the current page as PNG.""" dpi = request.args.get("dpi", "72", type=str) result = _send_command({"action": "screenshot", "args": {"dpi": int(dpi)}}) if "error" in result: return jsonify(result), 500 png_path = result.get("path") if png_path and os.path.exists(png_path): return app.send_static_file(png_path) if False else \ (open(png_path, "rb").read(), 200, {"Content-Type": "image/png"}) return jsonify({"error": "Screenshot not generated"}), 500 if __name__ == "__main__": DESIGNS_DIR.mkdir(parents=True, exist_ok=True) app.run(host="0.0.0.0", port=int(os.environ.get("BRIDGE_PORT", 8765)))