""" Export automation for mycopunk designs. Uses Inkscape CLI for SVG to PNG conversion. """ import subprocess import shutil from pathlib import Path from typing import Optional import yaml from rich.console import Console from rich.progress import Progress, SpinnerColumn, TextColumn console = Console() def get_project_root() -> Path: """Find the project root (directory containing designs/).""" current = Path.cwd() while current != current.parent: if (current / "designs").is_dir(): return current current = current.parent return Path.cwd() DESIGNS_DIR = get_project_root() / "designs" def check_inkscape() -> bool: """Check if Inkscape is available.""" return shutil.which("inkscape") is not None def export_design(path: str, dpi: int = 300, format: str = "png") -> None: """Export a design to print-ready files.""" design_dir = DESIGNS_DIR / path if not design_dir.exists(): console.print(f"[red]Error: Design not found at {design_dir}[/red]") raise SystemExit(1) metadata_path = design_dir / "metadata.yaml" if not metadata_path.exists(): console.print("[red]Error: metadata.yaml not found[/red]") raise SystemExit(1) with open(metadata_path) as f: metadata = yaml.safe_load(f) source_info = metadata.get("source", {}) source_file = source_info.get("file") if not source_file: console.print("[red]Error: No source file specified in metadata[/red]") raise SystemExit(1) source_path = design_dir / source_file if not source_path.exists(): console.print(f"[red]Error: Source file not found: {source_path}[/red]") raise SystemExit(1) # Determine export method based on file type if source_file.endswith(".svg"): export_svg(source_path, design_dir, metadata, dpi, format) elif source_file.endswith((".png", ".jpg", ".jpeg")): export_raster(source_path, design_dir, metadata, dpi, format) else: console.print(f"[red]Error: Unsupported source format: {source_file}[/red]") raise SystemExit(1) def export_svg( source_path: Path, design_dir: Path, metadata: dict, dpi: int, format: str ) -> None: """Export SVG using Inkscape.""" if not check_inkscape(): console.print("[red]Error: Inkscape not found. Please install Inkscape.[/red]") console.print("[dim]Ubuntu: sudo apt install inkscape[/dim]") raise SystemExit(1) source_info = metadata.get("source", {}) slug = source_path.stem # Create export directory export_dir = design_dir / "exports" / f"{dpi}dpi" export_dir.mkdir(parents=True, exist_ok=True) output_file = export_dir / f"{slug}.{format}" with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console, ) as progress: task = progress.add_task(f"Exporting {slug} at {dpi} DPI...", total=None) # Build Inkscape command cmd = [ "inkscape", str(source_path), f"--export-type={format}", f"--export-dpi={dpi}", f"--export-filename={output_file}", ] # Add transparent background for PNG if format == "png": cmd.append("--export-background-opacity=0") try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=120, # 2 minute timeout ) if result.returncode != 0: progress.stop() console.print(f"[red]Inkscape error:[/red]\n{result.stderr}") raise SystemExit(1) progress.update(task, description="Export complete!") except subprocess.TimeoutExpired: progress.stop() console.print("[red]Error: Export timed out[/red]") raise SystemExit(1) except FileNotFoundError: progress.stop() console.print("[red]Error: Inkscape not found[/red]") raise SystemExit(1) # Verify output if output_file.exists(): size_kb = output_file.stat().st_size / 1024 console.print(f"\n[green]✓ Exported:[/green] {output_file}") console.print(f"[dim] Size: {size_kb:.1f} KB[/dim]") # Try to get dimensions using PIL try: from PIL import Image with Image.open(output_file) as img: width, height = img.size console.print(f"[dim] Dimensions: {width}×{height} px[/dim]") except Exception: pass else: console.print("[red]Error: Export failed - output file not created[/red]") raise SystemExit(1) def export_raster( source_path: Path, design_dir: Path, metadata: dict, dpi: int, format: str ) -> None: """Export/convert raster images using PIL.""" from PIL import Image source_info = metadata.get("source", {}) slug = source_path.stem # Create export directory export_dir = design_dir / "exports" / f"{dpi}dpi" export_dir.mkdir(parents=True, exist_ok=True) output_file = export_dir / f"{slug}.{format}" with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console, ) as progress: task = progress.add_task(f"Processing {slug}...", total=None) try: with Image.open(source_path) as img: # Convert to appropriate mode if format == "png" and img.mode != "RGBA": img = img.convert("RGBA") elif format in ("jpg", "jpeg") and img.mode == "RGBA": # Flatten transparency for JPEG background = Image.new("RGB", img.size, (255, 255, 255)) background.paste(img, mask=img.split()[3]) img = background # Save with DPI metadata img.save(output_file, dpi=(dpi, dpi)) except Exception as e: progress.stop() console.print(f"[red]Error processing image: {e}[/red]") raise SystemExit(1) progress.update(task, description="Processing complete!") # Verify output if output_file.exists(): size_kb = output_file.stat().st_size / 1024 console.print(f"\n[green]✓ Exported:[/green] {output_file}") console.print(f"[dim] Size: {size_kb:.1f} KB[/dim]") with Image.open(output_file) as img: width, height = img.size console.print(f"[dim] Dimensions: {width}×{height} px[/dim]") else: console.print("[red]Error: Export failed[/red]") raise SystemExit(1) def batch_export( design_type: Optional[str] = None, status: str = "active", dpi: int = 300, format: str = "png" ) -> None: """Export all designs matching criteria.""" designs_exported = 0 errors = [] for category_dir in DESIGNS_DIR.iterdir(): if not category_dir.is_dir(): continue if design_type and category_dir.name != design_type: continue for design_dir in category_dir.iterdir(): if not design_dir.is_dir(): continue metadata_path = design_dir / "metadata.yaml" if not metadata_path.exists(): continue try: with open(metadata_path) as f: metadata = yaml.safe_load(f) except Exception: continue design_status = metadata.get("status", "unknown") if design_status != status: continue path = f"{category_dir.name}/{design_dir.name}" console.print(f"\n[bold]Exporting: {path}[/bold]") try: export_design(path, dpi, format) designs_exported += 1 except SystemExit: errors.append(path) console.print(f"\n[green]Exported {designs_exported} design(s)[/green]") if errors: console.print(f"[red]Failed: {len(errors)} design(s)[/red]") for e in errors: console.print(f" [red]• {e}[/red]")