This commit is contained in:
2026-03-19 14:54:09 +03:00
parent 7145b66e0e
commit 37036f8f75
26 changed files with 5466 additions and 0 deletions

182
Report/render_uml_png.py Normal file
View File

@@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""
Render PlantUML diagrams (.puml) to high-resolution PNG files via Kroki.
Why this script:
- local `plantuml` may require X11 in some environments;
- this script uses Kroki HTTP API and can raise output quality via `skinparam dpi`.
Examples:
python3 Report/render_uml_png.py
python3 Report/render_uml_png.py --dpi 360 --glob "*.puml"
python3 Report/render_uml_png.py --input-dir Report/uml --timeout 120
"""
from __future__ import annotations
import argparse
import io
from pathlib import Path
import re
from typing import Iterable
import requests
from PIL import Image
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Render .puml diagrams to PNG with configurable DPI."
)
parser.add_argument(
"--input-dir",
default="Report/uml",
help="Directory with .puml files (default: Report/uml)",
)
parser.add_argument(
"--glob",
default="*.puml",
help="Glob pattern inside input-dir (default: *.puml)",
)
parser.add_argument(
"--dpi",
type=int,
default=300,
help="Output DPI via PlantUML skinparam (default: 300)",
)
parser.add_argument(
"--timeout",
type=int,
default=120,
help="HTTP timeout in seconds (default: 120)",
)
parser.add_argument(
"--kroki-url",
default="https://kroki.io/plantuml/png",
help="Kroki PlantUML PNG endpoint",
)
parser.add_argument(
"--min-dpi",
type=int,
default=96,
help="Lower bound for auto DPI fallback (default: 96)",
)
parser.add_argument(
"--max-dim",
type=int,
default=3900,
help=(
"Target max PNG dimension during auto-fit. "
"Keep below backend hard limits (default: 3900)"
),
)
return parser.parse_args()
def iter_puml_files(input_dir: Path, pattern: str) -> Iterable[Path]:
return sorted(p for p in input_dir.glob(pattern) if p.is_file())
def inject_or_replace_skinparam(plantuml_text: str, key: str, value: int) -> str:
"""
Ensure diagram has the requested `skinparam <key>`.
- If skinparam exists, replace it.
- Otherwise insert after @startuml line.
"""
line_value = f"skinparam {key} {value}"
pattern = rf"(?im)^\s*skinparam\s+{re.escape(key)}\s+\d+\s*$"
if re.search(pattern, plantuml_text):
return re.sub(pattern, line_value, plantuml_text)
lines = plantuml_text.splitlines()
for i, line in enumerate(lines):
if line.strip().lower().startswith("@startuml"):
lines.insert(i + 1, line_value)
return "\n".join(lines) + ("\n" if plantuml_text.endswith("\n") else "")
# Fallback: if @startuml is missing, prepend anyway.
return line_value + "\n" + plantuml_text
def render_one(
puml_file: Path,
kroki_url: str,
dpi: int,
min_dpi: int,
max_dim: int,
timeout: int,
) -> Path:
source = puml_file.read_text(encoding="utf-8")
source = inject_or_replace_skinparam(source, "padding", 24)
current_dpi = dpi
png_bytes: bytes | None = None
while True:
payload = inject_or_replace_skinparam(source, "dpi", current_dpi)
response = requests.post(
kroki_url,
data=payload.encode("utf-8"),
timeout=timeout,
)
response.raise_for_status()
candidate = response.content
with Image.open(io.BytesIO(candidate)) as im:
width, height = im.size
# Kroki/PlantUML PNG responses may hit hard canvas limits.
# If we are close to that ceiling, lower DPI and retry.
if (width > max_dim or height > max_dim) and current_dpi > min_dpi:
next_dpi = max(min_dpi, int(current_dpi * 0.82))
if next_dpi == current_dpi:
next_dpi = current_dpi - 1
current_dpi = max(min_dpi, next_dpi)
continue
png_bytes = candidate
break
if png_bytes is None:
raise RuntimeError(f"Failed to render diagram: {puml_file}")
out_file = puml_file.with_suffix(".png")
out_file.write_bytes(png_bytes)
return out_file
def main() -> int:
args = parse_args()
input_dir = Path(args.input_dir).resolve()
if not input_dir.exists():
raise FileNotFoundError(f"Input directory not found: {input_dir}")
files = list(iter_puml_files(input_dir, args.glob))
if not files:
print(f"No files matched in {input_dir} with pattern {args.glob}")
return 0
print(f"Rendering {len(files)} diagrams from: {input_dir}")
print(f"DPI: {args.dpi}")
success = 0
for puml in files:
out = render_one(
puml_file=puml,
kroki_url=args.kroki_url,
dpi=args.dpi,
min_dpi=args.min_dpi,
max_dim=args.max_dim,
timeout=args.timeout,
)
print(f"created {out}")
success += 1
print(f"Done: {success}/{len(files)} PNG files generated.")
return 0
if __name__ == "__main__":
raise SystemExit(main())