Files
oci-deal-accelerator/tools/drawio_to_png.py
root b30a4f0d32 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>
2026-04-25 21:15:21 -03:00

170 lines
6.3 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.
#!/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()