forked from diegoecab/oci-deal-accelerator
Diagram generation: ref-arch-driven procedure + spec validator + KB enrichment
The diagram path now follows a documented standard procedure (lookup the closest Oracle Architecture Center reference → confirm components → author absolute_layout → spec validator → render → visually verify) and ships persistent guardrails so layout regressions can't recur. Persistent procedure changes (apply to all users, all sessions): - tools/diagram_spec_validator.py — geometry checks (CONTAINER_TOO_THIN, CONTAINER_PADDING_VIOLATION, LABEL_OVERFLOW_PARENT) run BEFORE either renderer (drawio + PPTX). Catches the subnet-collapse / label-overflow bugs that the post-render drawio validator missed. - tools/oci_diagram_gen.py + tools/oci_pptx_diagram_gen.py — call the spec validator before emitting any output. Adds mysql / mysql_heatwave type aliases. - tools/archcenter_pattern_lookup.py — scores against cached page descriptions (not just the 1-line summary), supports --queries for multi-fragment composition, and applies synonym expansion via kb/architecture-center/synonyms.yaml so "LB HA cross AD" matches "load balancer high availability availability domain". - kb/architecture-center/synonyms.yaml — canonical synonym table (load balancer, autonomous database, data guard, …) used by the lookup scorer. KB enrichment: - tools/archcenter_description_fetcher.py + 121 cached _description.md under kb/diagram/assets/archcenter-refs/<slug>/. Removes the runtime dependency on docs.oracle.com when authoring specs and feeds the pattern-lookup scorer. - 110+ cached .drawio / .svg / .png references for offline reuse, plus the OCI Toolkit v24.2 import (kb/diagram/assets/oci-toolkit-drawio). Documentation: - docs/skill/output-formats.md — new "Standard diagram-generation procedure (MANDATORY)" + geometry rules + the new validator entry. - SKILL.md option 2 — references the mandatory procedure. - README.md — describes the spec validator, archcenter_pattern_lookup and description fetcher, and updates the KB-health table. Tooling that backs the procedure (cumulative across recent sessions): tools/archcenter_case_runner.py, archcenter_batch_driver.py, archcenter_zip_downloader.py, drawio_visual_validator.py, drawio_fidelity_eval.py, harvest_drawio_icon.py, import_oci_library.py, oci_pptx_diagram_gen.py, oci_pptx_render.py, refresh_pptx_icon_index.py. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
169
tools/drawio_to_png.py
Normal file
169
tools/drawio_to_png.py
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
drawio_to_png — render a .drawio file to PNG using drawio.exe (Windows
|
||||
under WSL) or a Linux drawio binary if available.
|
||||
|
||||
Why this matters: the SVG-based fidelity eval uses Oracle's bundled
|
||||
SVG companion (same source as the official drawio). It validates that
|
||||
the *verbatim* drawio is byte-identical to the reference, but it does
|
||||
NOT validate the *rebuilt* drawio (the spec → drawio path). Only by
|
||||
exporting our generated drawio to PNG and diffing it against the
|
||||
canonical PNG do we catch bugs introduced by the rebuild — e.g. a
|
||||
700pt label that the SVG-based fidelity test never sees.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# Order matters: prefer the Windows drawio executable if available
|
||||
# under WSL; fall back to a Linux binary if installed.
|
||||
def _candidate_binaries() -> list[Path]:
|
||||
"""Locate a draw.io binary. The skill is shared, so we *never* probe
|
||||
machine-specific WSL paths automatically — that would silently
|
||||
activate a Windows-only path on the original developer's box and
|
||||
silently disable it everywhere else. Opt-in only via:
|
||||
|
||||
- ``DRAWIO_EXE`` env var (full path to the binary), or
|
||||
- a ``drawio`` / ``draw.io`` binary on PATH.
|
||||
"""
|
||||
cands: list[Path] = []
|
||||
env_path = os.environ.get("DRAWIO_EXE", "")
|
||||
if env_path:
|
||||
cands.append(Path(env_path))
|
||||
for name in ("drawio", "draw.io"):
|
||||
which = shutil.which(name)
|
||||
if which:
|
||||
cands.append(Path(which))
|
||||
return cands
|
||||
|
||||
|
||||
def find_drawio_binary() -> Path | None:
|
||||
for p in _candidate_binaries():
|
||||
if p.exists():
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def _to_windows_path(path: Path) -> str:
|
||||
"""Map a /mnt/c/... path to C:\\... so drawio.exe (running under
|
||||
Windows from WSL) can read/write it."""
|
||||
abs_path = str(path.resolve())
|
||||
if abs_path.startswith("/mnt/"):
|
||||
# /mnt/c/Users/... → C:\Users\...
|
||||
m = re.match(r"^/mnt/([a-zA-Z])(/.*)?$", abs_path)
|
||||
if m:
|
||||
drive = m.group(1).upper()
|
||||
rest = (m.group(2) or "").replace("/", "\\")
|
||||
return f"{drive}:{rest}"
|
||||
return abs_path
|
||||
|
||||
|
||||
def _read_page_size(drawio_path: Path) -> tuple[int, int] | None:
|
||||
"""Read pageWidth/pageHeight from the drawio so we can crop the export
|
||||
to the canvas. drawio.exe expands to fit *all* content by default; if a
|
||||
cell sits a few pixels outside the page (common for outline edges of
|
||||
Oracle stencils) the PNG ends up much wider than the diagram, which
|
||||
destroys raster fidelity comparisons.
|
||||
"""
|
||||
try:
|
||||
text = Path(drawio_path).read_text(encoding="utf-8", errors="ignore")
|
||||
except OSError:
|
||||
return None
|
||||
import re as _re
|
||||
m = _re.search(r'pageWidth="(\d+)"[^>]*pageHeight="(\d+)"', text)
|
||||
if not m:
|
||||
m = _re.search(r'pageHeight="(\d+)"[^>]*pageWidth="(\d+)"', text)
|
||||
if m:
|
||||
return int(m.group(2)), int(m.group(1))
|
||||
return None
|
||||
return int(m.group(1)), int(m.group(2))
|
||||
|
||||
|
||||
def render(drawio_path: Path, out_png: Path, scale: int = 2,
|
||||
binary: Path | None = None,
|
||||
crop_to_page: bool = True) -> None:
|
||||
binary = binary or find_drawio_binary()
|
||||
if binary is None:
|
||||
raise RuntimeError(
|
||||
"drawio binary not found. Install draw.io for Windows "
|
||||
"(/mnt/c/Program Files/draw.io/draw.io.exe) or `dnf install drawio`."
|
||||
)
|
||||
out_png.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
page_size = _read_page_size(drawio_path) if crop_to_page else None
|
||||
width_arg = ["-s", str(scale)]
|
||||
# Note: we deliberately use the bare -s scale flag rather than
|
||||
# --width/--height. With explicit dimensions, drawio.exe's behavior
|
||||
# diverges across versions (some treat them as DPI multipliers),
|
||||
# producing renders 3-4x larger than the page. Plain -s consistently
|
||||
# yields page_w*scale × page_h*scale.
|
||||
is_windows_binary = ".exe" in str(binary)
|
||||
if is_windows_binary:
|
||||
# drawio.exe under WSL needs Windows-visible paths. Stage to a
|
||||
# /mnt/c/-rooted scratch dir if either input or output sits on a
|
||||
# WSL-only filesystem.
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
stage_dir = project_root / "tmp" / "drawio-render-stage"
|
||||
stage_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _stage(p: Path, copy_in: bool) -> Path:
|
||||
if str(p).startswith("/mnt/"):
|
||||
return p
|
||||
staged = stage_dir / p.name
|
||||
if copy_in:
|
||||
shutil.copy(p, staged)
|
||||
return staged
|
||||
|
||||
staged = _stage(drawio_path, copy_in=True)
|
||||
staged_out = _stage(out_png, copy_in=False)
|
||||
# Note: deliberately NOT passing --crop. With --crop drawio.exe
|
||||
# rasterizes the bounding box of all cells, not the page — if a
|
||||
# single stencil overflows the canvas by a few pixels (common
|
||||
# with Oracle's stenciled icons), the export ends up wider than
|
||||
# the canonical PNG and similarity comparisons get destroyed by
|
||||
# the aspect-ratio mismatch.
|
||||
cmd = [
|
||||
str(binary), "-x", "-f", "png",
|
||||
*width_arg,
|
||||
"-o", _to_windows_path(staged_out),
|
||||
_to_windows_path(staged),
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode != 0 or not staged_out.exists():
|
||||
raise RuntimeError(
|
||||
f"drawio.exe failed (rc={result.returncode}): "
|
||||
f"stdout={result.stdout!r} stderr={result.stderr!r}"
|
||||
)
|
||||
if staged_out != out_png:
|
||||
shutil.move(staged_out, out_png)
|
||||
else:
|
||||
cmd = [
|
||||
str(binary), "-x", "-f", "png",
|
||||
"-s", str(scale),
|
||||
"-o", str(out_png),
|
||||
str(drawio_path),
|
||||
]
|
||||
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Render a .drawio file to PNG.")
|
||||
parser.add_argument("--drawio", required=True, type=Path)
|
||||
parser.add_argument("--out", required=True, type=Path)
|
||||
parser.add_argument("--scale", type=int, default=2)
|
||||
args = parser.parse_args()
|
||||
render(args.drawio, args.out, scale=args.scale)
|
||||
print(f"wrote {args.out} ({args.out.stat().st_size} bytes)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user