mycopunk-swag/cli/mycopunk/export.py

269 lines
8.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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]")