154 lines
5.3 KiB
Python
154 lines
5.3 KiB
Python
"""Dithering API endpoints for on-demand dithering and screen-print separations."""
|
|
|
|
import io
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from fastapi.responses import StreamingResponse
|
|
|
|
from app.schemas.dither import (
|
|
DitherAlgorithm,
|
|
DitherResponse,
|
|
PaletteMode,
|
|
ScreenPrintExport,
|
|
ScreenPrintRequest,
|
|
)
|
|
from app.services.design_service import DesignService
|
|
from app.services.dither_service import dither_design, generate_color_separations
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
design_service = DesignService()
|
|
|
|
|
|
@router.get("/{slug}/dither")
|
|
async def get_dithered_design(
|
|
slug: str,
|
|
algorithm: DitherAlgorithm = DitherAlgorithm.FLOYD_STEINBERG,
|
|
palette: PaletteMode = PaletteMode.AUTO,
|
|
num_colors: int = Query(default=8, ge=2, le=32),
|
|
colors: str | None = Query(default=None, description="Comma-separated hex colors for custom palette"),
|
|
threshold: int = Query(default=64, ge=1, le=256),
|
|
order: int = Query(default=8, ge=2, le=16),
|
|
fresh: bool = False,
|
|
format: str = Query(default="image", description="'image' for PNG, 'json' for metadata"),
|
|
):
|
|
"""Apply dithering to a design image.
|
|
|
|
Returns a PNG image (default) or JSON metadata with image URL.
|
|
"""
|
|
image_path = await design_service.get_design_image_path(slug)
|
|
if not image_path or not Path(image_path).exists():
|
|
raise HTTPException(status_code=404, detail="Design not found")
|
|
|
|
custom_colors = [c.strip() for c in colors.split(",")] if colors else None
|
|
|
|
try:
|
|
png_bytes, meta = dither_design(
|
|
image_path=image_path,
|
|
slug=slug,
|
|
algorithm=algorithm,
|
|
palette_mode=palette,
|
|
num_colors=num_colors,
|
|
custom_colors=custom_colors,
|
|
threshold=threshold,
|
|
order=order,
|
|
fresh=fresh,
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Dithering failed for {slug}: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Dithering failed: {e}")
|
|
|
|
if format == "json":
|
|
return DitherResponse(
|
|
slug=slug,
|
|
algorithm=algorithm,
|
|
palette_mode=palette,
|
|
num_colors=meta["num_colors"],
|
|
colors_used=meta["colors_used"],
|
|
cached=meta["cached"],
|
|
image_url=f"/api/designs/{slug}/dither?algorithm={algorithm.value}&palette={palette.value}&num_colors={num_colors}",
|
|
)
|
|
|
|
return StreamingResponse(
|
|
io.BytesIO(png_bytes),
|
|
media_type="image/png",
|
|
headers={"Cache-Control": "public, max-age=86400"},
|
|
)
|
|
|
|
|
|
@router.post("/{slug}/screen-print", response_model=ScreenPrintExport)
|
|
async def create_screen_print(slug: str, body: ScreenPrintRequest):
|
|
"""Generate color separations for screen printing."""
|
|
image_path = await design_service.get_design_image_path(slug)
|
|
if not image_path or not Path(image_path).exists():
|
|
raise HTTPException(status_code=404, detail="Design not found")
|
|
|
|
try:
|
|
composite_bytes, separations, colors = generate_color_separations(
|
|
image_path=image_path,
|
|
slug=slug,
|
|
num_colors=body.num_colors,
|
|
algorithm=body.algorithm,
|
|
spot_colors=body.spot_colors,
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Screen print generation failed for {slug}: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Screen print generation failed: {e}")
|
|
|
|
base_url = f"/api/designs/{slug}/screen-print"
|
|
separation_urls = {color: f"{base_url}/{color}" for color in colors}
|
|
|
|
return ScreenPrintExport(
|
|
slug=slug,
|
|
num_colors=len(colors),
|
|
algorithm=body.algorithm,
|
|
colors=colors,
|
|
composite_url=f"{base_url}/composite",
|
|
separation_urls=separation_urls,
|
|
)
|
|
|
|
|
|
@router.get("/{slug}/screen-print/{channel}")
|
|
async def get_screen_print_channel(slug: str, channel: str):
|
|
"""Serve a screen-print separation channel image.
|
|
|
|
channel: "composite" or a hex color code (e.g. "FF0000")
|
|
"""
|
|
image_path = await design_service.get_design_image_path(slug)
|
|
if not image_path or not Path(image_path).exists():
|
|
raise HTTPException(status_code=404, detail="Design not found")
|
|
|
|
# Try to find cached separations — generate with defaults if not cached
|
|
try:
|
|
composite_bytes, separations, colors = generate_color_separations(
|
|
image_path=image_path,
|
|
slug=slug,
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Screen print channel failed for {slug}/{channel}: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to generate separation: {e}")
|
|
|
|
if channel == "composite":
|
|
png_bytes = composite_bytes
|
|
filename = f"{slug}-composite.png"
|
|
else:
|
|
hex_upper = channel.upper().lstrip("#")
|
|
if hex_upper not in separations:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Color channel '{channel}' not found. Available: {', '.join(separations.keys())}",
|
|
)
|
|
png_bytes = separations[hex_upper]
|
|
filename = f"{slug}-{hex_upper}.png"
|
|
|
|
return StreamingResponse(
|
|
io.BytesIO(png_bytes),
|
|
media_type="image/png",
|
|
headers={
|
|
"Cache-Control": "public, max-age=86400",
|
|
"Content-Disposition": f"attachment; filename={filename}",
|
|
},
|
|
)
|