104 lines
3.9 KiB
Python
104 lines
3.9 KiB
Python
"""把 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)
|