#!/usr/bin/env python3 """Renders images/icon.svg to a square PNG (512x512 by default). Stdlib only: the actual rasterising is delegated to the first renderer found, in this order: the cairosvg Python package, rsvg-convert (librsvg), ImageMagick (magick/convert), Inkscape. Install any one of them. Usage: tools/icon_png.py # images/icon.svg -> images/icon.png, 512x512 tools/icon_png.py --resolution 1024 # 1024x1024 tools/icon_png.py other.svg -o out.png # explicit input/output The output is always PNG, always x. """ from __future__ import annotations import argparse import shutil import subprocess import sys from pathlib import Path from typing import List, Optional REPO_ROOT = Path(__file__).resolve().parent.parent DEFAULT_SVG = REPO_ROOT / "images" / "icon.svg" DEFAULT_RESOLUTION = 512 def fail(message: str) -> "None": print(f"error: {message}", file=sys.stderr) raise SystemExit(1) def cli_command(svg: Path, png: Path, size: int) -> Optional[List[str]]: """Returns the command line for the first installed CLI renderer, if any.""" if shutil.which("rsvg-convert"): return ["rsvg-convert", "-w", str(size), "-h", str(size), "-f", "png", "-o", str(png), str(svg)] magick = shutil.which("magick") or shutil.which("convert") if magick: # A high density rasterises the vector sharply before scaling down; # "!" forces the exact size. return [magick, "-background", "none", "-density", "1200", str(svg), "-resize", f"{size}x{size}!", f"png:{png}"] if shutil.which("inkscape"): return ["inkscape", str(svg), "--export-type=png", f"--export-filename={png}", "-w", str(size), "-h", str(size)] return None def render(svg: Path, png: Path, size: int) -> str: """Renders svg into png at size x size, returning the renderer's name.""" try: import cairosvg # type: ignore[import-not-found] except ImportError: pass else: cairosvg.svg2png(url=str(svg), write_to=str(png), output_width=size, output_height=size) return "cairosvg" command = cli_command(svg, png, size) if command is None: fail("no SVG renderer found; install one of: cairosvg (pip), " "rsvg-convert (librsvg), ImageMagick, Inkscape") result = subprocess.run(command, capture_output=True, text=True) if result.returncode != 0: fail(f"{Path(command[0]).name} failed:\n{result.stderr.strip()}") return Path(command[0]).name def main() -> None: parser = argparse.ArgumentParser(description="Render an SVG icon to a square PNG.") parser.add_argument("svg", nargs="?", type=Path, default=DEFAULT_SVG, help="input SVG (default: images/icon.svg)") parser.add_argument("-r", "--resolution", type=int, default=DEFAULT_RESOLUTION, help=f"width and height in pixels (default: {DEFAULT_RESOLUTION})") parser.add_argument("-o", "--output", type=Path, help="output PNG (default: the input path with a .png suffix)") args = parser.parse_args() if args.resolution <= 0: fail("--resolution must be a positive integer") if not args.svg.is_file(): fail(f"{args.svg} not found") png = args.output or args.svg.with_suffix(".png") if png.suffix.lower() != ".png": fail(f"output must be a .png file, got {png}") png.parent.mkdir(parents=True, exist_ok=True) renderer = render(args.svg, png, args.resolution) print(f"{png} ({args.resolution}x{args.resolution}, via {renderer})") if __name__ == "__main__": main()