269 lines
8.1 KiB
Python
269 lines
8.1 KiB
Python
"""
|
||
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]")
|