package api import ( "errors" "fmt" "net/http" "printer.backend/internal/model" "printer.backend/internal/platepdf" "printer.backend/internal/store" ) func registerPDFRoutes(mux *http.ServeMux, configs *store.ConfigurationStore, plates *store.PlateStore, items *store.ItemStore) { mux.HandleFunc("GET /layout/pdf", layoutPDF(plates, items)) mux.HandleFunc("GET /configurations/{id}/pdf", configurationPDF(configs, plates, items)) } func layoutPDF(plates *store.PlateStore, items *store.ItemStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { plateID := r.URL.Query().Get("plate_id") itemID := r.URL.Query().Get("item_id") if plateID == "" || itemID == "" { writeError(w, http.StatusBadRequest, errors.New("plate_id and item_id query params are required")) return } spacing, err := parseSpacing(r.URL.Query().Get("spacing_mm")) if err != nil { writeError(w, http.StatusBadRequest, err) return } plate, item, preview, svgPath, err := resolveLayoutPDF(plates, items, plateID, itemID, spacing) if err != nil { writeError(w, http.StatusBadRequest, err) return } pdf, err := platepdf.Generate(plate, item.Spec, svgPath, preview) if err != nil { writeError(w, http.StatusInternalServerError, err) return } servePDF(w, pdf, "layout-preview.pdf") } } func configurationPDF(configs *store.ConfigurationStore, plates *store.PlateStore, items *store.ItemStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") c, err := configs.Get(id) if err != nil { writeError(w, http.StatusNotFound, err) return } plate, item, preview, svgPath, err := resolveLayoutPDF(plates, items, c.PlateID, c.ItemID, c.SpacingMM) if err != nil { writeError(w, http.StatusBadRequest, err) return } pdf, err := platepdf.Generate(plate, item.Spec, svgPath, preview) if err != nil { writeError(w, http.StatusInternalServerError, err) return } name := c.Name if name == "" { name = "configuration-" + id[:8] } servePDF(w, pdf, sanitizeFilename(name)+".pdf") } } func resolveLayoutPDF(plates *store.PlateStore, items *store.ItemStore, plateID, itemID string, spacingMM float64) ( plate model.Plate, item model.Item, preview model.LayoutPreview, svgPath string, err error, ) { plate, err = findPlate(plates, plateID) if err != nil { return plate, item, preview, "", err } item, err = items.Get(itemID) if err != nil { return plate, item, preview, "", err } svgPath, err = items.SVGPath(itemID) if err != nil { return plate, item, preview, "", err } preview, err = buildPreview(plates, items, plateID, itemID, spacingMM) if err != nil { return plate, item, preview, "", err } preview.PlateID = plateID preview.ItemID = itemID return plate, item, preview, svgPath, nil } func servePDF(w http.ResponseWriter, pdf []byte, filename string) { w.Header().Set("Content-Type", "application/pdf") w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) w.Header().Set("Cache-Control", "no-cache") _, _ = w.Write(pdf) } func sanitizeFilename(name string) string { var b []byte for i := 0; i < len(name); i++ { c := name[i] if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' { b = append(b, c) } else if c == ' ' { b = append(b, '_') } } if len(b) == 0 { return "preview" } return string(b) }