Created
March 18, 2026 14:58
-
-
Save pHo9UBenaA/43e0ed489dfab9ea14303cadf36077e2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # Run: uv run python drawio-to-png-tabs.py <input_dir> <output_dir> | |
| # Output: | |
| # input/path/sample.drawio | |
| # -> out/input/path/sample/<tabname>.png | |
| from __future__ import annotations | |
| import argparse | |
| import os | |
| import re | |
| import shutil | |
| import subprocess | |
| import sys | |
| import xml.etree.ElementTree as ET | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| DRAWIO_CANDIDATES = ("drawio", "draw.io", "draw") | |
| @dataclass(frozen=True) | |
| class ExportSummary: | |
| exported_files: int | |
| exported_tabs: int | |
| def sanitize_filename(name: str) -> str: | |
| sanitized_name = name.strip() | |
| sanitized_name = re.sub(r'[\\/:*?"<>|]', "_", sanitized_name) | |
| sanitized_name = re.sub(r"\s+", " ", sanitized_name) | |
| sanitized_name = sanitized_name.rstrip(".") | |
| return sanitized_name or "untitled" | |
| def unique_name(base_name: str, used_names: set[str]) -> str: | |
| if base_name not in used_names: | |
| used_names.add(base_name) | |
| return base_name | |
| suffix = 2 | |
| while True: | |
| candidate_name = f"{base_name}_{suffix}" | |
| if candidate_name not in used_names: | |
| used_names.add(candidate_name) | |
| return candidate_name | |
| suffix += 1 | |
| def resolve_drawio_cmd(explicit_cmd: str | None = None) -> str: | |
| candidates = [] | |
| if explicit_cmd: | |
| candidates.append(explicit_cmd) | |
| if env_value := os.environ.get("DRAWIO_CMD"): | |
| candidates.append(env_value) | |
| candidates.extend(DRAWIO_CANDIDATES) | |
| for candidate in candidates: | |
| if not candidate: | |
| continue | |
| resolved_path = shutil.which(candidate) | |
| if resolved_path: | |
| return resolved_path | |
| candidate_path = Path(candidate) | |
| if candidate_path.exists(): | |
| return str(candidate_path) | |
| raise FileNotFoundError( | |
| "draw.io CLI was not found. Add `drawio`, `draw.io`, or `draw` to PATH, " | |
| "or provide `--drawio-cmd` or `DRAWIO_CMD`." | |
| ) | |
| def iter_drawio_files(input_dir: Path) -> list[Path]: | |
| return sorted(path for path in input_dir.rglob("*.drawio") if path.is_file()) | |
| def get_tab_names(drawio_file: Path) -> list[str]: | |
| try: | |
| diagrams = ET.parse(drawio_file).getroot().findall(".//diagram") | |
| except ET.ParseError as error: | |
| raise RuntimeError(f"Failed to parse XML: {drawio_file}\n{error}") from error | |
| if not diagrams: | |
| raise RuntimeError(f"No <diagram> elements were found: {drawio_file}") | |
| used_names: set[str] = set() | |
| tab_names: list[str] = [] | |
| for page_index, diagram in enumerate(diagrams, start=1): | |
| raw_name = diagram.attrib.get("name") or f"tab_{page_index}" | |
| tab_names.append(unique_name(sanitize_filename(raw_name), used_names)) | |
| return tab_names | |
| def export_tab_png(drawio_cmd: str, src: Path, page_index: int, out_png: Path) -> None: | |
| out_png.parent.mkdir(parents=True, exist_ok=True) | |
| command = [ | |
| drawio_cmd, | |
| "--export", | |
| "--format", | |
| "png", | |
| "--crop", | |
| "--page-index", | |
| str(page_index), | |
| "--output", | |
| str(out_png), | |
| str(src), | |
| ] | |
| result = subprocess.run( | |
| command, | |
| capture_output=True, | |
| text=True, | |
| check=False, | |
| ) | |
| if result.returncode != 0: | |
| raise RuntimeError( | |
| "PNG export failed\n" | |
| f"src={src}\n" | |
| f"page_index={page_index}\n" | |
| f"out={out_png}\n" | |
| f"cmd={' '.join(command)}\n" | |
| f"stdout:\n{result.stdout}\n" | |
| f"stderr:\n{result.stderr}" | |
| ) | |
| if not out_png.exists(): | |
| raise RuntimeError(f"The command succeeded, but no output file was created: {out_png}") | |
| def convert_drawio_file(drawio_cmd: str, input_dir: Path, output_dir: Path, src: Path) -> int: | |
| relative_path = src.relative_to(input_dir) | |
| relative_dir = relative_path.with_suffix("") | |
| destination_dir = output_dir / relative_dir | |
| tab_names = get_tab_names(src) | |
| print(f"[INFO] {src}: {len(tab_names)} tabs") | |
| for page_index, tab_name in enumerate(tab_names, start=1): | |
| out_png = destination_dir / f"{tab_name}.png" | |
| export_tab_png(drawio_cmd, src, page_index, out_png) | |
| print(f" [OK] {out_png}") | |
| return len(tab_names) | |
| def convert_directory(input_dir: Path, output_dir: Path, drawio_cmd: str) -> ExportSummary: | |
| drawio_files = iter_drawio_files(input_dir) | |
| if not drawio_files: | |
| print(f"[WARN] No .drawio files were found: {input_dir}") | |
| return ExportSummary(exported_files=0, exported_tabs=0) | |
| exported_tabs = 0 | |
| for src in drawio_files: | |
| exported_tabs += convert_drawio_file(drawio_cmd, input_dir, output_dir, src) | |
| return ExportSummary(exported_files=len(drawio_files), exported_tabs=exported_tabs) | |
| def build_parser() -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser( | |
| description="Export each draw.io tab as an individual tabname.png file.", | |
| ) | |
| parser.add_argument("input_dir", type=Path, help="Input directory containing .drawio files") | |
| parser.add_argument("output_dir", type=Path, help="Output directory for generated PNG files") | |
| parser.add_argument( | |
| "--drawio-cmd", | |
| default=None, | |
| help="draw.io CLI command or absolute path", | |
| ) | |
| return parser | |
| def main(argv: list[str] | None = None) -> int: | |
| args = build_parser().parse_args(argv) | |
| input_dir = args.input_dir | |
| output_dir = args.output_dir | |
| if not input_dir.is_dir(): | |
| print(f"[ERROR] Input directory does not exist: {input_dir}", file=sys.stderr) | |
| return 1 | |
| try: | |
| drawio_cmd = resolve_drawio_cmd(args.drawio_cmd) | |
| summary = convert_directory(input_dir, output_dir, drawio_cmd) | |
| except (FileNotFoundError, RuntimeError, OSError) as error: | |
| print(f"[ERROR] {error}", file=sys.stderr) | |
| return 1 | |
| print("") | |
| print("===== summary =====") | |
| print(f"input dir : {input_dir.resolve()}") | |
| print(f"output root : {output_dir.resolve()}") | |
| print(f"drawio cli : {drawio_cmd}") | |
| print(f"files done : {summary.exported_files}") | |
| print(f"tabs exported: {summary.exported_tabs}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment