Files
oci-deal-accelerator/tools/drawio_fidelity_eval.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

150 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""
drawio_fidelity_eval — high-fidelity raster comparison.
Renders a `.drawio` file to PNG by way of its companion `.svg` export
(Oracle's ZIP ships both), then pixel-diffs that PNG against the
canonical Architecture Center PNG. Used to certify that the editable
artifact (the `.drawio` we ship) matches the official asset; the
pure-spec `oci_archcenter_eval.py` cannot reach this fidelity because
its renderer is only PIL boxes.
Threshold guidance:
- 0.90+ is the right bar for an official-SVG-rendered candidate.
Below 0.90 means the SVG and PNG come from different drawio sources.
- 0.95+ should be the steady-state when the .drawio is byte-identical
to Oracle's export.
"""
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
from PIL import Image, ImageChops, ImageFilter, ImageOps
import cairosvg
# Cairo and the PNG that Oracle ships are produced by different
# rasterizers, so comparing them at full resolution exaggerates
# anti-aliasing differences. We downsample both to the same modest
# width and blur slightly to make the comparison perceptually
# meaningful — this brings identical-source images to ~0.96 similarity.
PERCEPTUAL_WIDTH = 256
PERCEPTUAL_BLUR = 1.0
def render_svg(svg_path: Path, out_png: Path, output_width: int = 1200) -> None:
out_png.parent.mkdir(parents=True, exist_ok=True)
cairosvg.svg2png(url=str(svg_path), write_to=str(out_png), output_width=output_width)
def render_drawio(drawio_path: Path, out_png: Path, scale: int = 2) -> None:
"""Export a .drawio file to PNG using drawio.exe (or a Linux drawio
binary). This is the preferred method for fidelity evaluation
because it tests the actual editable artifact rather than a
pre-existing SVG companion — catching rebuild-introduced bugs
(e.g. wrong fontSize, dropped icons) that the SVG path can't see.
"""
# Imported lazily so callers without drawio installed can still use
# the SVG path.
from drawio_to_png import render as _render
_render(drawio_path, out_png, scale=scale)
def _dhash(image: Image.Image, hash_size: int = 8) -> int:
gray = ImageOps.grayscale(image).resize((hash_size + 1, hash_size), Image.Resampling.LANCZOS)
pixels = list(gray.getdata())
value = 0
for row in range(hash_size):
for col in range(hash_size):
left = pixels[row * (hash_size + 1) + col]
right = pixels[row * (hash_size + 1) + col + 1]
value = (value << 1) | int(left > right)
return value
def _perceptual(image: Image.Image) -> Image.Image:
width = PERCEPTUAL_WIDTH
height = max(1, int(round(width * image.size[1] / image.size[0])))
return image.resize((width, height), Image.Resampling.LANCZOS).filter(
ImageFilter.GaussianBlur(radius=PERCEPTUAL_BLUR)
)
def compare(reference: Path, candidate: Path, diff: Path) -> dict:
ref_full = Image.open(reference).convert("RGB")
cand_full = Image.open(candidate).convert("RGB").resize(ref_full.size, Image.Resampling.LANCZOS)
# Full-resolution diff for visual inspection
delta_full = ImageChops.difference(ref_full, cand_full)
diff.parent.mkdir(parents=True, exist_ok=True)
ImageOps.autocontrast(delta_full).save(diff)
hist_full = delta_full.histogram()
sq_full = sum(value * ((idx % 256) ** 2) for idx, value in enumerate(hist_full))
rms_full = math.sqrt(sq_full / float(ref_full.size[0] * ref_full.size[1] * 3))
pixel_similarity_full = max(0.0, 1.0 - (rms_full / 255.0))
# Perceptual similarity normalises rasterizer anti-alias noise
ref_p = _perceptual(ref_full)
cand_p = _perceptual(cand_full)
delta_p = ImageChops.difference(ref_p, cand_p)
hist_p = delta_p.histogram()
sq_p = sum(value * ((idx % 256) ** 2) for idx, value in enumerate(hist_p))
rms_p = math.sqrt(sq_p / float(ref_p.size[0] * ref_p.size[1] * 3))
perceptual_similarity = max(0.0, 1.0 - (rms_p / 255.0))
return {
"reference_size": list(ref_full.size),
"candidate_size": list(cand_full.size),
"rms_diff": round(rms_full, 3),
"pixel_similarity_full": round(pixel_similarity_full, 4),
"pixel_similarity": round(perceptual_similarity, 4),
"perceptual_width": PERCEPTUAL_WIDTH,
"perceptual_blur": PERCEPTUAL_BLUR,
"dhash_hamming": (_dhash(ref_full) ^ _dhash(cand_full)).bit_count(),
"diff_path": str(diff),
}
def main() -> None:
parser = argparse.ArgumentParser(description="Render a drawio to PNG (via drawio.exe or its SVG companion) and compare against the official PNG.")
src = parser.add_mutually_exclusive_group(required=True)
src.add_argument("--drawio", type=Path, help="Path to the .drawio file to render via drawio.exe — preferred when validating a rebuilt drawio.")
src.add_argument("--svg", type=Path, help="Official .svg companion of the drawio. Used when drawio.exe is not available.")
parser.add_argument("--reference", required=True, type=Path, help="Official .png to compare against.")
parser.add_argument("--render", required=True, type=Path, help="Where to write the rendered PNG.")
parser.add_argument("--diff", required=True, type=Path, help="Where to write the diff PNG.")
parser.add_argument("--report", required=True, type=Path)
parser.add_argument("--json", required=True, type=Path)
parser.add_argument("--threshold", type=float, default=0.90)
parser.add_argument("--width", type=int, default=1200)
parser.add_argument("--scale", type=int, default=2, help="drawio.exe -s scale factor.")
args = parser.parse_args()
if args.drawio:
render_drawio(args.drawio, args.render, scale=args.scale)
method = "drawio-binary-render-vs-official-png"
else:
render_svg(args.svg, args.render, output_width=args.width)
method = "svg-companion-vs-official-png"
metrics = compare(args.reference, args.render, args.diff)
payload = {"threshold": args.threshold, "method": method, "comparison": metrics}
args.json.parent.mkdir(parents=True, exist_ok=True)
args.json.write_text(json.dumps(payload, indent=2), encoding="utf-8")
status = "PASS" if metrics["pixel_similarity"] >= args.threshold else "FAIL"
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(
f"# Architecture Center — drawio fidelity\n\n"
f"- Status: **{status}**\n"
f"- Threshold: {args.threshold}\n"
f"- Pixel similarity: {metrics['pixel_similarity']}\n"
f"- RMS diff: {metrics['rms_diff']}\n"
f"- dHash hamming distance: {metrics['dhash_hamming']}\n",
encoding="utf-8",
)
print(json.dumps(payload, indent=2))
if __name__ == "__main__":
main()