"""把 drawio 框图渲染成 PNG,供原理图第一页当插图(n10 硬要求)。 两种 drawio 格式都吃: * SVG 外壳(drawio MCP / 导出的 .drawio.svg):headless Chrome 直接截 SVG; * 纯 mxfile XML(drawio 桌面版存的 .drawio):套一层 HTML,用 diagrams.net 的 viewer-static.min.js 在 headless Chrome 里渲染后截图(需要能上网取 JS)。 不用 draw.io.exe --export:G-0022 实测它在 Windows 上报 input file not found,还会挂起。 python -m schgen.diagram [--scale 2] """ import html import io import os import re import struct import subprocess import sys import tempfile from .env import chrome VIEWER_JS = "https://viewer.diagrams.net/js/viewer-static.min.js" def _png_size(p): d = io.open(p, "rb").read(32) return struct.unpack(">II", d[16:24]) def _page_size(xml): m = re.search(r'pageWidth="(\d+)"[^>]*pageHeight="(\d+)"', xml) if m: return int(m.group(1)), int(m.group(2)) return 1600, 1200 def _bounds(xml): """所有顶点几何的外包框,用来裁掉页面空白。""" xs, ys = [], [] for m in re.finditer(r']*?)/?>', xml): a = dict(re.findall(r'(\w+)="([^"]*)"', m.group(1))) if a.get("relative") == "1" or "width" not in a: continue x, y = float(a.get("x", 0)), float(a.get("y", 0)) w, h = float(a["width"]), float(a["height"]) xs += [x, x + w]; ys += [y, y + h] if not xs: return None return min(xs), min(ys), max(xs), max(ys) def render(src, png, scale=2, margin=20): raw = io.open(src, encoding="utf-8", errors="replace").read() tmpdir = tempfile.mkdtemp(prefix="kf_diag_") if raw.lstrip().startswith("]*\swidth="([\d.]+)(?:px)?"[^>]*\sheight="([\d.]+)', raw) w, h = (int(float(m.group(1))), int(float(m.group(2)))) if m else (1600, 1200) url = "file:///" + os.path.abspath(src).replace("\\", "/") budget = [] else: xml = raw bb = _bounds(xml) if bb: w, h = int(bb[2] - bb[0] + 2 * margin), int(bb[3] - bb[1] + 2 * margin) else: w, h = _page_size(xml) cfg = '{"highlight":"#0000ff","nav":false,"resize":false,"toolbar":"","edit":null,' \ '"lightbox":false,"border":%d,"xml":%s}' % (margin, _json_str(xml)) page = ('' '' '
' '' % (html.escape(cfg, quote=True), VIEWER_JS)) hp = os.path.join(tmpdir, "d.html") io.open(hp, "w", encoding="utf-8").write(page) url = "file:///" + hp.replace("\\", "/") budget = ["--virtual-time-budget=15000"] if os.path.exists(png): os.remove(png) os.makedirs(os.path.dirname(os.path.abspath(png)), exist_ok=True) cmd = [chrome(), "--headless", "--disable-gpu", "--hide-scrollbars", "--force-device-scale-factor=%d" % scale, "--default-background-color=FFFFFFFF", "--window-size=%d,%d" % (w, h), "--screenshot=%s" % os.path.abspath(png)] + budget + [url] subprocess.run(cmd, check=True, capture_output=True, timeout=120) if not os.path.exists(png): raise SystemExit("截图失败:%s" % png) pw, ph = _png_size(png) print("框图 -> %s %dx%d px(KiCad 按 300DPI 解释:scale=1 时 %.0fx%.0f mm)" % (png, pw, ph, pw / 300.0 * 25.4, ph / 300.0 * 25.4)) return png def _json_str(s): import json return json.dumps(s) if __name__ == "__main__": a = sys.argv[1:] sc = 2 if "--scale" in a: i = a.index("--scale"); sc = int(a[i + 1]); del a[i:i + 2] render(a[0], a[1], scale=sc)