175 lines
5.8 KiB
Python
175 lines
5.8 KiB
Python
"""API routes for the Kerf Engine trace and simplify endpoints."""
|
|
|
|
import json
|
|
import time
|
|
|
|
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
|
|
from fastapi.responses import Response
|
|
|
|
from output import generate_dxf, generate_json, generate_svg
|
|
from pipeline.postprocess import postprocess_svg
|
|
from pipeline.preprocessing import preprocess
|
|
from pipeline.vectorize import potrace_trace, vtracer_trace
|
|
|
|
router = APIRouter()
|
|
|
|
VALID_MODES = {"potrace", "vtracer"}
|
|
VALID_OUTPUT_FORMATS = {"svg", "dxf", "json"}
|
|
|
|
|
|
def _format_response(
|
|
result,
|
|
output_format: str,
|
|
warnings: list[str],
|
|
processing_ms: float,
|
|
):
|
|
"""Build a standardized response from a PostProcessResult and output format.
|
|
|
|
SVG and JSON return a JSON envelope with output + metadata.
|
|
DXF returns raw bytes with application/dxf content type.
|
|
"""
|
|
metadata = {
|
|
"format": output_format,
|
|
"path_count": len(result.paths),
|
|
"node_count_total": result.total_nodes,
|
|
"open_paths": result.open_path_count,
|
|
"island_count": result.island_count,
|
|
"warnings": warnings,
|
|
"processing_ms": processing_ms,
|
|
}
|
|
|
|
if output_format == "dxf":
|
|
dxf_bytes = generate_dxf(result)
|
|
return Response(
|
|
content=dxf_bytes,
|
|
media_type="application/dxf",
|
|
headers={
|
|
"Content-Disposition": "attachment; filename=output.dxf",
|
|
"X-Kerf-Metadata": json.dumps(metadata),
|
|
},
|
|
)
|
|
elif output_format == "json":
|
|
return {
|
|
"output": json.loads(generate_json(result)),
|
|
"format": "json",
|
|
"metadata": metadata,
|
|
}
|
|
else:
|
|
return {
|
|
"output": generate_svg(result),
|
|
"format": "svg",
|
|
"metadata": metadata,
|
|
}
|
|
|
|
|
|
@router.post("/engine/trace")
|
|
async def trace(
|
|
file: UploadFile = File(...),
|
|
mode: str = Form("potrace"),
|
|
output_format: str = Form("svg"),
|
|
preset: str = Form("default"),
|
|
params: str = Form("{}"),
|
|
):
|
|
"""Convert a raster image to vector via the preprocessing + vectorization + post-processing pipeline.
|
|
|
|
Supports three output formats: svg (default), dxf, json.
|
|
"""
|
|
if mode not in VALID_MODES:
|
|
raise HTTPException(
|
|
status_code=422,
|
|
detail=f"Invalid mode '{mode}'. Must be one of: {', '.join(sorted(VALID_MODES))}",
|
|
)
|
|
|
|
if output_format not in VALID_OUTPUT_FORMATS:
|
|
raise HTTPException(
|
|
status_code=422,
|
|
detail=f"Unsupported output_format '{output_format}'. Must be one of: {', '.join(sorted(VALID_OUTPUT_FORMATS))}",
|
|
)
|
|
|
|
try:
|
|
user_params = json.loads(params)
|
|
except json.JSONDecodeError as exc:
|
|
raise HTTPException(status_code=422, detail=f"Invalid params JSON: {exc}")
|
|
|
|
raw_bytes = await file.read()
|
|
if not raw_bytes:
|
|
raise HTTPException(status_code=422, detail="Uploaded file is empty")
|
|
|
|
warnings: list[str] = []
|
|
start = time.perf_counter()
|
|
|
|
try:
|
|
preprocessed = preprocess(raw_bytes, params=user_params)
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=422, detail=f"Preprocessing failed: {exc}")
|
|
|
|
try:
|
|
if mode == "potrace":
|
|
svg_output = potrace_trace(preprocessed, **{
|
|
k: v for k, v in user_params.items()
|
|
if k in ("turdsize", "alphamax", "opticurve", "opttolerance")
|
|
})
|
|
else:
|
|
svg_output = vtracer_trace(preprocessed, **{
|
|
k: v for k, v in user_params.items()
|
|
if k in (
|
|
"colormode", "hierarchical", "filter_speckle", "color_precision",
|
|
"layer_difference", "corner_threshold", "length_threshold",
|
|
"splice_threshold", "mode", "path_precision", "max_iterations",
|
|
)
|
|
})
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=f"Vectorization failed: {exc}")
|
|
|
|
# Post-processing: RDP simplification, island detection, open path analysis
|
|
epsilon = float(user_params.get("epsilon", 1.0))
|
|
try:
|
|
result = postprocess_svg(svg_output, epsilon=epsilon)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=f"Post-processing failed: {exc}")
|
|
|
|
processing_ms = round((time.perf_counter() - start) * 1000, 2)
|
|
|
|
return _format_response(result, output_format, warnings, processing_ms)
|
|
|
|
|
|
@router.post("/engine/simplify")
|
|
async def simplify(
|
|
file: UploadFile = File(...),
|
|
epsilon: float = Form(1.0),
|
|
output_format: str = Form("svg"),
|
|
):
|
|
"""Simplify an existing SVG using RDP path simplification.
|
|
|
|
Accepts an SVG file upload, applies Ramer-Douglas-Peucker simplification
|
|
with the given epsilon, and returns the result in the requested format.
|
|
"""
|
|
if output_format not in VALID_OUTPUT_FORMATS:
|
|
raise HTTPException(
|
|
status_code=422,
|
|
detail=f"Unsupported output_format '{output_format}'. Must be one of: {', '.join(sorted(VALID_OUTPUT_FORMATS))}",
|
|
)
|
|
|
|
raw_bytes = await file.read()
|
|
if not raw_bytes:
|
|
raise HTTPException(status_code=422, detail="Uploaded file is empty")
|
|
|
|
try:
|
|
svg_str = raw_bytes.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
raise HTTPException(status_code=422, detail="File is not valid UTF-8 text")
|
|
|
|
if "<svg" not in svg_str.lower():
|
|
raise HTTPException(status_code=422, detail="File does not appear to be a valid SVG")
|
|
|
|
warnings: list[str] = []
|
|
start = time.perf_counter()
|
|
|
|
try:
|
|
result = postprocess_svg(svg_str, epsilon=epsilon)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=500, detail=f"Simplification failed: {exc}")
|
|
|
|
processing_ms = round((time.perf_counter() - start) * 1000, 2)
|
|
|
|
return _format_response(result, output_format, warnings, processing_ms)
|