| """ |
| mermaid-renderer — Convert Mermaid diagrams to images using @mermaid-js/mermaid-cli |
| Author: algorembrant |
| License: MIT |
| |
| USAGE COMMANDS |
| ============== |
| |
| Single file: |
| python render.py diagram.mmd |
| python render.py diagram.mmd -o output.png |
| python render.py diagram.mmd -o output.svg |
| python render.py diagram.mmd -o output.pdf |
| |
| Inline diagram: |
| python render.py --text "graph TD; A --> B --> C" |
| python render.py --text "sequenceDiagram; Alice->>Bob: Hello" |
| |
| Themes (15 built-in from beautiful-mermaid): |
| python render.py diagram.mmd --theme tokyo-night |
| python render.py diagram.mmd --theme catppuccin-mocha |
| python render.py diagram.mmd --theme github-light |
| python render.py diagram.mmd --theme dracula |
| python render.py diagram.mmd --theme nord |
| python render.py diagram.mmd --theme one-dark |
| |
| Custom colors: |
| python render.py diagram.mmd --bg "#1a1b26" --fg "#a9b1d6" |
| python render.py diagram.mmd --bg "#ffffff" --fg "#333333" --accent "#0969da" |
| |
| Background: |
| python render.py diagram.mmd --background white |
| python render.py diagram.mmd --background transparent |
| python render.py diagram.mmd --background "#1e1e2e" |
| |
| Scale / quality: |
| python render.py diagram.mmd --scale 2 |
| python render.py diagram.mmd --scale 3 |
| python render.py diagram.mmd --width 1920 |
| |
| Batch (directory or glob): |
| python render.py --batch ./diagrams/ |
| python render.py --batch ./diagrams/ -o ./output/ --format png |
| python render.py --batch "./diagrams/*.mmd" --theme tokyo-night --scale 2 |
| |
| Batch parallel workers: |
| python render.py --batch ./diagrams/ --workers 4 |
| |
| Watch mode (re-render on change): |
| python render.py diagram.mmd --watch |
| python render.py --batch ./diagrams/ --watch |
| |
| List built-in themes: |
| python render.py --list-themes |
| |
| Check environment / dependencies: |
| python render.py --check |
| |
| Verbose logging: |
| python render.py diagram.mmd -v |
| python render.py --batch ./diagrams/ -v --workers 2 |
| |
| Help: |
| python render.py --help |
| """ |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import glob |
| import json |
| import logging |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| from concurrent.futures import ProcessPoolExecutor, as_completed |
| from pathlib import Path |
| from typing import Optional |
|
|
| |
| |
| |
| logging.basicConfig( |
| level=logging.WARNING, |
| format="[%(levelname)s] %(message)s", |
| ) |
| log = logging.getLogger("mermaid-renderer") |
|
|
|
|
| |
| |
| |
| THEMES: dict[str, dict] = { |
| "default": { |
| "bg": "#FFFFFF", |
| "fg": "#27272A", |
| "mermaid_theme": "default", |
| }, |
| "zinc-light": { |
| "bg": "#FFFFFF", |
| "fg": "#18181B", |
| "mermaid_theme": "default", |
| }, |
| "zinc-dark": { |
| "bg": "#18181B", |
| "fg": "#E4E4E7", |
| "mermaid_theme": "dark", |
| }, |
| "tokyo-night": { |
| "bg": "#1a1b26", |
| "fg": "#a9b1d6", |
| "accent": "#7aa2f7", |
| "mermaid_theme": "dark", |
| }, |
| "tokyo-night-storm": { |
| "bg": "#24283b", |
| "fg": "#c0caf5", |
| "accent": "#7aa2f7", |
| "mermaid_theme": "dark", |
| }, |
| "tokyo-night-light": { |
| "bg": "#d5d6db", |
| "fg": "#343b58", |
| "accent": "#34548a", |
| "mermaid_theme": "default", |
| }, |
| "catppuccin-mocha": { |
| "bg": "#1e1e2e", |
| "fg": "#cdd6f4", |
| "accent": "#cba6f7", |
| "mermaid_theme": "dark", |
| }, |
| "catppuccin-latte": { |
| "bg": "#eff1f5", |
| "fg": "#4c4f69", |
| "accent": "#8839ef", |
| "mermaid_theme": "default", |
| }, |
| "nord": { |
| "bg": "#2e3440", |
| "fg": "#d8dee9", |
| "accent": "#88c0d0", |
| "mermaid_theme": "dark", |
| }, |
| "nord-light": { |
| "bg": "#eceff4", |
| "fg": "#2e3440", |
| "accent": "#5e81ac", |
| "mermaid_theme": "default", |
| }, |
| "dracula": { |
| "bg": "#282a36", |
| "fg": "#f8f8f2", |
| "accent": "#bd93f9", |
| "mermaid_theme": "dark", |
| }, |
| "github-light": { |
| "bg": "#ffffff", |
| "fg": "#1F2328", |
| "accent": "#0969da", |
| "mermaid_theme": "default", |
| }, |
| "github-dark": { |
| "bg": "#0d1117", |
| "fg": "#e6edf3", |
| "accent": "#4493f8", |
| "mermaid_theme": "dark", |
| }, |
| "solarized-light": { |
| "bg": "#fdf6e3", |
| "fg": "#657b83", |
| "accent": "#268bd2", |
| "mermaid_theme": "default", |
| }, |
| "solarized-dark": { |
| "bg": "#002b36", |
| "fg": "#839496", |
| "accent": "#268bd2", |
| "mermaid_theme": "dark", |
| }, |
| "one-dark": { |
| "bg": "#282c34", |
| "fg": "#abb2bf", |
| "accent": "#c678dd", |
| "mermaid_theme": "dark", |
| }, |
| } |
|
|
|
|
| |
| |
| |
|
|
| def find_mmdc() -> Optional[str]: |
| """Return path to mmdc binary or None.""" |
| |
| candidates = [ |
| shutil.which("mmdc"), |
| shutil.which("mmdc.cmd"), |
| os.path.expanduser("~/.npm-global/bin/mmdc"), |
| os.path.join(os.environ.get("APPDATA", ""), "npm", "mmdc.cmd"), |
| "/usr/local/bin/mmdc", |
| "/usr/bin/mmdc", |
| ] |
| for c in candidates: |
| if c and os.path.isfile(c): |
| return c |
| return None |
|
|
|
|
| def find_chrome() -> Optional[str]: |
| """Return path to a Chromium/Chrome binary or None.""" |
| candidates = [ |
| |
| r"C:\Program Files\Google\Chrome\Application\chrome.exe", |
| r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", |
| os.path.join(os.environ.get("LOCALAPPDATA", ""), r"Google\Chrome\Application\chrome.exe"), |
| |
| os.path.expanduser( |
| "~/.cache/puppeteer/chrome/linux-131.0.6778.204/chrome-linux64/chrome" |
| ), |
| "/opt/google/chrome/chrome", |
| "/opt/pw-browsers/chromium-1194/chrome-linux/chrome", |
| shutil.which("google-chrome"), |
| shutil.which("chromium"), |
| shutil.which("chromium-browser"), |
| shutil.which("chrome"), |
| ] |
| for c in candidates: |
| if c and os.path.isfile(c): |
| return c |
| return None |
|
|
|
|
| def build_puppeteer_config(chrome_path: str) -> str: |
| """Write a puppeteer config JSON to a temp file, return its path.""" |
| cfg = { |
| "executablePath": chrome_path, |
| "args": [ |
| "--no-sandbox", |
| "--disable-setuid-sandbox", |
| "--disable-dev-shm-usage", |
| "--disable-gpu", |
| ], |
| } |
| fd, path = tempfile.mkstemp(suffix=".json", prefix="puppeteer_cfg_") |
| with os.fdopen(fd, "w") as f: |
| json.dump(cfg, f) |
| return path |
|
|
|
|
| def build_mermaid_config(theme_cfg: dict, bg: Optional[str]) -> str: |
| """Write a mermaid config JSON, return its path.""" |
| mermaid_theme = theme_cfg.get("mermaid_theme", "default") |
| cfg: dict = { |
| "theme": mermaid_theme, |
| } |
| if mermaid_theme == "dark": |
| cfg["themeVariables"] = { |
| "darkMode": True, |
| "background": theme_cfg.get("bg", "#1e1e2e"), |
| "primaryColor": theme_cfg.get("accent", theme_cfg.get("fg", "#88c0d0")), |
| "primaryTextColor": theme_cfg.get("fg", "#d8dee9"), |
| "lineColor": theme_cfg.get("accent", "#88c0d0"), |
| } |
| else: |
| cfg["themeVariables"] = { |
| "darkMode": False, |
| "background": theme_cfg.get("bg", "#ffffff"), |
| "primaryColor": theme_cfg.get("accent", "#ececff"), |
| "primaryTextColor": theme_cfg.get("fg", "#333333"), |
| "lineColor": theme_cfg.get("accent", "#333333"), |
| } |
|
|
| if bg: |
| cfg["themeVariables"]["background"] = bg |
|
|
| fd, path = tempfile.mkstemp(suffix=".json", prefix="mermaid_cfg_") |
| with os.fdopen(fd, "w") as f: |
| json.dump(cfg, f) |
| return path |
|
|
|
|
| |
| |
| |
|
|
| def render_diagram( |
| source: str, |
| output_path: str, |
| *, |
| mmdc: str, |
| puppeteer_cfg: str, |
| theme_name: str = "default", |
| bg: Optional[str] = None, |
| scale: float = 2.0, |
| width: Optional[int] = None, |
| fmt: str = "png", |
| verbose: bool = False, |
| ) -> dict: |
| """ |
| Render a Mermaid diagram string to an image file. |
| |
| Parameters |
| ---------- |
| source : Mermaid diagram text |
| output_path : Destination file path |
| mmdc : Path to mmdc binary |
| puppeteer_cfg: Path to puppeteer config JSON |
| theme_name : One of THEMES keys or 'default' |
| bg : Override background color (hex or 'transparent') |
| scale : Device pixel ratio (default 2 = 2x) |
| width : Optional canvas width in pixels |
| fmt : Output format: png | svg | pdf |
| verbose : Log mmdc stdout/stderr |
| |
| Returns |
| ------- |
| dict with keys: success, output, error, duration_ms |
| """ |
| theme_cfg = THEMES.get(theme_name, THEMES["default"]) |
| t0 = time.perf_counter() |
|
|
| |
| fd, src_path = tempfile.mkstemp(suffix=".mmd", prefix="mermaid_src_") |
| mermaid_cfg_path = None |
| try: |
| with os.fdopen(fd, "w") as f: |
| f.write(source) |
|
|
| mermaid_cfg_path = build_mermaid_config(theme_cfg, bg) |
|
|
| cmd = [ |
| mmdc, |
| "-i", src_path, |
| "-o", output_path, |
| "--puppeteerConfigFile", puppeteer_cfg, |
| "--configFile", mermaid_cfg_path, |
| "--scale", str(scale), |
| ] |
|
|
| if bg == "transparent": |
| cmd += ["-b", "transparent"] |
| elif bg: |
| cmd += ["-b", bg] |
| elif "bg" in theme_cfg: |
| cmd += ["-b", theme_cfg["bg"]] |
|
|
| if width: |
| cmd += ["-w", str(width)] |
|
|
| if fmt == "pdf": |
| cmd += ["--pdfFit"] |
|
|
| log.debug("CMD: %s", " ".join(cmd)) |
|
|
| result = subprocess.run( |
| cmd, |
| capture_output=True, |
| text=True, |
| timeout=60, |
| ) |
|
|
| duration_ms = int((time.perf_counter() - t0) * 1000) |
|
|
| if verbose: |
| if result.stdout.strip(): |
| print(f" [mmdc stdout] {result.stdout.strip()}") |
| if result.stderr.strip(): |
| print(f" [mmdc stderr] {result.stderr.strip()}") |
|
|
| success = result.returncode == 0 and os.path.isfile(output_path) |
| error = None |
| if not success: |
| error = (result.stderr or result.stdout or "mmdc exited with code " |
| + str(result.returncode)).strip() |
|
|
| return { |
| "success": success, |
| "output": output_path if success else None, |
| "error": error, |
| "duration_ms": duration_ms, |
| } |
|
|
| except subprocess.TimeoutExpired: |
| return { |
| "success": False, |
| "output": None, |
| "error": "Render timed out (60s)", |
| "duration_ms": int((time.perf_counter() - t0) * 1000), |
| } |
| except Exception as exc: |
| return { |
| "success": False, |
| "output": None, |
| "error": str(exc), |
| "duration_ms": int((time.perf_counter() - t0) * 1000), |
| } |
| finally: |
| try: |
| os.unlink(src_path) |
| except OSError: |
| pass |
| if mermaid_cfg_path: |
| try: |
| os.unlink(mermaid_cfg_path) |
| except OSError: |
| pass |
|
|
|
|
| |
| |
| |
|
|
| def _batch_worker(args: tuple) -> dict: |
| """Top-level function for ProcessPoolExecutor (must be picklable).""" |
| ( |
| src_file, out_file, mmdc, puppeteer_cfg, |
| theme, bg, scale, width, fmt, verbose, |
| ) = args |
| source = Path(src_file).read_text(encoding="utf-8") |
| return { |
| "input": src_file, |
| **render_diagram( |
| source, out_file, |
| mmdc=mmdc, puppeteer_cfg=puppeteer_cfg, |
| theme_name=theme, bg=bg, scale=scale, |
| width=width, fmt=fmt, verbose=verbose, |
| ), |
| } |
|
|
|
|
| def resolve_batch_inputs(batch_path: str, fmt: str, out_dir: Optional[str]) -> list[tuple[str, str]]: |
| """Return list of (input_path, output_path) tuples for a batch run.""" |
| p = Path(batch_path) |
| pairs: list[tuple[str, str]] = [] |
|
|
| if "*" in batch_path or "?" in batch_path: |
| inputs = sorted(glob.glob(batch_path)) |
| elif p.is_dir(): |
| inputs = sorted( |
| str(f) for f in p.rglob("*") |
| if f.suffix.lower() in {".mmd", ".mermaid", ".txt"} |
| and f.is_file() |
| ) |
| elif p.is_file(): |
| inputs = [str(p)] |
| else: |
| print(f"[ERROR] Batch path not found: {batch_path}", file=sys.stderr) |
| return [] |
|
|
| for inp in inputs: |
| in_path = Path(inp) |
| if out_dir: |
| out_base = Path(out_dir) |
| out_base.mkdir(parents=True, exist_ok=True) |
| out_file = str(out_base / (in_path.stem + f".{fmt}")) |
| else: |
| out_file = str(in_path.with_suffix(f".{fmt}")) |
| pairs.append((inp, out_file)) |
|
|
| return pairs |
|
|
|
|
| |
| |
| |
|
|
| def watch_file(path: str) -> float: |
| try: |
| return os.path.getmtime(path) |
| except OSError: |
| return 0.0 |
|
|
|
|
| def run_watch( |
| input_files: list[str], |
| output_map: dict[str, str], |
| render_kwargs: dict, |
| poll_interval: float = 0.8, |
| ) -> None: |
| """Poll input files and re-render on modification.""" |
| mtimes = {f: watch_file(f) for f in input_files} |
| print(f"Watching {len(input_files)} file(s). Press Ctrl+C to stop.") |
| try: |
| while True: |
| time.sleep(poll_interval) |
| for f in input_files: |
| mtime = watch_file(f) |
| if mtime != mtimes[f]: |
| mtimes[f] = mtime |
| print(f" Changed: {f} — re-rendering...") |
| source = Path(f).read_text(encoding="utf-8") |
| result = render_diagram(source, output_map[f], **render_kwargs) |
| if result["success"]: |
| print(f" OK: {result['output']} ({result['duration_ms']}ms)") |
| else: |
| print(f" FAIL: {result['error']}", file=sys.stderr) |
| except KeyboardInterrupt: |
| print("\nWatch mode stopped.") |
|
|
|
|
| |
| |
| |
|
|
| def build_parser() -> argparse.ArgumentParser: |
| p = argparse.ArgumentParser( |
| prog="mermaid-renderer", |
| description="Convert Mermaid diagrams to PNG, SVG, or PDF images.", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=__doc__, |
| ) |
|
|
| p.add_argument( |
| "input", |
| nargs="?", |
| help="Path to .mmd / .mermaid file (omit when using --text or --batch)", |
| ) |
| p.add_argument( |
| "-o", "--output", |
| default=None, |
| help="Output file path (default: same name as input, auto-extension)", |
| ) |
| p.add_argument( |
| "--text", |
| default=None, |
| help="Inline Mermaid diagram text (alternative to file input)", |
| ) |
| p.add_argument( |
| "--batch", |
| default=None, |
| metavar="DIR_OR_GLOB", |
| help="Render all .mmd/.mermaid files in a directory or matching a glob", |
| ) |
| p.add_argument( |
| "--format", "-f", |
| default="png", |
| choices=["png", "svg", "pdf"], |
| help="Output format (default: png)", |
| ) |
| p.add_argument( |
| "--theme", "-t", |
| default="default", |
| choices=list(THEMES.keys()), |
| help="Named theme (default: default)", |
| ) |
| p.add_argument( |
| "--bg", "--background", |
| default=None, |
| dest="bg", |
| help='Background color hex or "transparent"', |
| ) |
| p.add_argument( |
| "--fg", "--foreground", |
| default=None, |
| dest="fg", |
| help="Foreground color hex (overrides theme fg — informational only)", |
| ) |
| p.add_argument( |
| "--accent", |
| default=None, |
| help="Accent color hex (overrides theme accent — informational only)", |
| ) |
| p.add_argument( |
| "--scale", |
| type=float, |
| default=2.0, |
| help="Device pixel ratio / scale factor (default: 2.0)", |
| ) |
| p.add_argument( |
| "--width", "-W", |
| type=int, |
| default=None, |
| help="Canvas width in pixels", |
| ) |
| p.add_argument( |
| "--workers", "-j", |
| type=int, |
| default=1, |
| help="Parallel workers for batch mode (default: 1)", |
| ) |
| p.add_argument( |
| "--watch", "-w", |
| action="store_true", |
| help="Watch input files and re-render on change", |
| ) |
| p.add_argument( |
| "--list-themes", |
| action="store_true", |
| help="Print all available themes and exit", |
| ) |
| p.add_argument( |
| "--check", |
| action="store_true", |
| help="Check environment dependencies and exit", |
| ) |
| p.add_argument( |
| "-v", "--verbose", |
| action="store_true", |
| help="Show mmdc stdout/stderr", |
| ) |
|
|
| return p |
|
|
|
|
| def cmd_check() -> None: |
| """Print environment diagnostic info.""" |
| mmdc = find_mmdc() |
| chrome = find_chrome() |
|
|
| print("mermaid-renderer — environment check") |
| print("-" * 40) |
| print(f" Python : {sys.version.split()[0]}") |
| print(f" mmdc : {mmdc or 'NOT FOUND'}") |
| print(f" Chrome/Chrom: {chrome or 'NOT FOUND'}") |
|
|
| if mmdc: |
| try: |
| r = subprocess.run([mmdc, "--version"], capture_output=True, text=True, timeout=5) |
| print(f" mmdc version: {r.stdout.strip()}") |
| except Exception: |
| print(" mmdc version: (could not query)") |
|
|
| if mmdc and chrome: |
| print("\n All dependencies OK. Rendering should work.") |
| else: |
| missing = [] |
| if not mmdc: |
| missing.append("mmdc (install: npm install -g @mermaid-js/mermaid-cli)") |
| if not chrome: |
| missing.append("Chrome/Chromium") |
| print("\n MISSING:", ", ".join(missing)) |
| sys.exit(1) |
|
|
|
|
| def cmd_list_themes() -> None: |
| col_w = 24 |
| print(f"\n{'Theme':<{col_w}} {'Type':<8} {'Background':<12} {'Accent'}") |
| print("-" * 64) |
| for name, cfg in THEMES.items(): |
| theme_type = "dark" if cfg.get("mermaid_theme") == "dark" else "light" |
| accent = cfg.get("accent", "(derived)") |
| print(f" {name:<{col_w-2}} {theme_type:<8} {cfg.get('bg',''):<12} {accent}") |
| print() |
|
|
|
|
| def main() -> None: |
| parser = build_parser() |
| args = parser.parse_args() |
|
|
| if args.verbose: |
| log.setLevel(logging.DEBUG) |
|
|
| if args.check: |
| cmd_check() |
| return |
|
|
| if args.list_themes: |
| cmd_list_themes() |
| return |
|
|
| |
| mmdc = find_mmdc() |
| chrome = find_chrome() |
|
|
| if not mmdc: |
| print( |
| "[ERROR] mmdc not found. Install with:\n" |
| " npm install -g @mermaid-js/mermaid-cli\n" |
| "Then run: python render.py --check", |
| file=sys.stderr, |
| ) |
| sys.exit(1) |
|
|
| if not chrome: |
| print( |
| "[ERROR] No Chrome/Chromium found. See README for install instructions.", |
| file=sys.stderr, |
| ) |
| sys.exit(1) |
|
|
| puppeteer_cfg = build_puppeteer_config(chrome) |
|
|
| |
| theme_cfg = dict(THEMES.get(args.theme, THEMES["default"])) |
| if args.fg: |
| theme_cfg["fg"] = args.fg |
| if args.accent: |
| theme_cfg["accent"] = args.accent |
|
|
| render_kwargs = dict( |
| mmdc=mmdc, |
| puppeteer_cfg=puppeteer_cfg, |
| theme_name=args.theme, |
| bg=args.bg, |
| scale=args.scale, |
| width=args.width, |
| fmt=args.format, |
| verbose=args.verbose, |
| ) |
|
|
| try: |
| |
| |
| |
| if args.batch: |
| pairs = resolve_batch_inputs(args.batch, args.format, args.output) |
| if not pairs: |
| print("[ERROR] No input files found.", file=sys.stderr) |
| sys.exit(1) |
|
|
| print(f"Batch: {len(pairs)} file(s) | theme={args.theme} | " |
| f"format={args.format} | workers={args.workers}") |
|
|
| if args.watch: |
| input_files = [p[0] for p in pairs] |
| output_map = {p[0]: p[1] for p in pairs} |
| run_watch(input_files, output_map, render_kwargs) |
| return |
|
|
| if args.workers > 1: |
| worker_args = [ |
| (inp, out, mmdc, puppeteer_cfg, |
| args.theme, args.bg, args.scale, args.width, |
| args.format, args.verbose) |
| for inp, out in pairs |
| ] |
| results = [] |
| with ProcessPoolExecutor(max_workers=args.workers) as ex: |
| futures = {ex.submit(_batch_worker, a): a[0] for a in worker_args} |
| for fut in as_completed(futures): |
| results.append(fut.result()) |
| else: |
| results = [] |
| for inp, out in pairs: |
| source = Path(inp).read_text(encoding="utf-8") |
| r = render_diagram(source, out, **render_kwargs) |
| results.append({"input": inp, **r}) |
| status = "OK" if r["success"] else "FAIL" |
| print(f" [{status}] {inp} -> {out} ({r['duration_ms']}ms)") |
| if not r["success"]: |
| print(f" Error: {r['error']}", file=sys.stderr) |
|
|
| ok = sum(1 for r in results if r["success"]) |
| fail = len(results) - ok |
| print(f"\nDone: {ok} succeeded, {fail} failed.") |
| if fail > 0: |
| sys.exit(1) |
| return |
|
|
| |
| |
| |
| if args.text: |
| source = args.text.replace("; ", "\n").replace(";", "\n") |
| default_stem = "diagram" |
| elif args.input: |
| in_path = Path(args.input) |
| if not in_path.is_file(): |
| print(f"[ERROR] Input file not found: {args.input}", file=sys.stderr) |
| sys.exit(1) |
| source = in_path.read_text(encoding="utf-8") |
| default_stem = in_path.stem |
| else: |
| parser.print_help() |
| sys.exit(0) |
|
|
| |
| if args.output: |
| out_path = args.output |
| else: |
| suffix = f".{args.format}" |
| out_path = str(Path(default_stem).with_suffix(suffix)) |
|
|
| print(f"Rendering: {out_path} | theme={args.theme} | format={args.format} | scale={args.scale}") |
|
|
| result = render_diagram(source, out_path, **render_kwargs) |
|
|
| if result["success"]: |
| size = os.path.getsize(result["output"]) |
| print(f" OK: {result['output']} ({size:,} bytes, {result['duration_ms']}ms)") |
| else: |
| print(f" FAIL: {result['error']}", file=sys.stderr) |
| sys.exit(1) |
|
|
| |
| if args.watch and args.input: |
| run_watch([args.input], {args.input: out_path}, render_kwargs) |
|
|
| finally: |
| try: |
| os.unlink(puppeteer_cfg) |
| except OSError: |
| pass |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|