Files
skills-win/kicad-flow/scripts/schgen/diagram.py
T
2026-10-15 01:59:40 +08:00

104 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""把 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 <in.drawio> <out.png> [--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'<mxGeometry\s+([^>]*?)/?>', 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("<svg") or "<svg" in raw[:500]:
m = re.search(r'<svg[^>]*\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 = ('<!DOCTYPE html><html><head><meta charset="utf-8">'
'<style>html,body{margin:0;padding:0;background:#fff}</style></head><body>'
'<div class="mxgraph" style="max-width:100%%" data-mxgraph="%s"></div>'
'<script src="%s"></script></body></html>' % (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)