forked from diegoecab/oci-deal-accelerator
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>
170 lines
6.3 KiB
Python
170 lines
6.3 KiB
Python
#!/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()
|