Added Printjobs

This commit is contained in:
2026-05-26 18:27:37 +02:00
parent 929969d03a
commit 2432a34198
17 changed files with 1255 additions and 8 deletions
+63
View File
@@ -0,0 +1,63 @@
package printjob
import (
"fmt"
"printer.backend/internal/model"
)
// CollectImageRefs returns all order images in job order (orders, then images within each order).
func CollectImageRefs(orders []model.Order) []model.PrintJobImageRef {
var refs []model.PrintJobImageRef
for _, o := range orders {
for _, img := range o.Images {
refs = append(refs, model.PrintJobImageRef{
OrderID: o.ID,
ImageID: img.ID,
})
}
}
return refs
}
// AssignSlots maps images to layout slots (at most one image per slot).
func AssignSlots(refs []model.PrintJobImageRef, preview model.LayoutPreview) []model.PrintJobSlotAssignment {
limit := len(preview.Positions)
if len(refs) < limit {
limit = len(refs)
}
out := make([]model.PrintJobSlotAssignment, 0, limit)
for i := 0; i < limit; i++ {
out = append(out, model.PrintJobSlotAssignment{
Slot: i,
Image: refs[i],
Position: preview.Positions[i],
})
}
return out
}
// Summary builds validation and slot assignments for a print job.
func Summary(orders []model.Order, preview model.LayoutPreview) model.PrintJobSummary {
refs := CollectImageRefs(orders)
slots := preview.Count
imageCount := len(refs)
overflow := imageCount > slots
var warning string
if overflow {
warning = fmt.Sprintf(
"%d Bilder für %d Item-Positionen: %d Bilder werden nicht platziert",
imageCount, slots, imageCount-slots,
)
}
return model.PrintJobSummary{
ImageCount: imageCount,
SlotCount: slots,
ImageOverflow: overflow,
Warning: warning,
Assignments: AssignSlots(refs, preview),
Preview: preview,
}
}
+56
View File
@@ -0,0 +1,56 @@
package printjob
import (
"strings"
"testing"
"printer.backend/internal/layout"
"printer.backend/internal/model"
)
func TestSummaryOverflowWarning(t *testing.T) {
plate := model.Plate{
WidthMM: 100, HeightMM: 100,
MarginTop: 5, MarginRight: 5, MarginBottom: 5, MarginLeft: 5,
}
spec := model.ItemSpec{WidthMM: 80, HeightMM: 80, BleedMM: 2, MarginMM: 5, PaddingMM: 3}
preview := layout.Pack(plate, spec, 0)
orders := []model.Order{
{ID: "o1", Images: []model.OrderImage{{ID: "a"}, {ID: "b"}}},
{ID: "o2", Images: []model.OrderImage{{ID: "c"}}},
}
s := Summary(orders, preview)
if preview.Count == 0 {
if s.ImageCount != 3 {
t.Fatalf("expected 3 images, got %d", s.ImageCount)
}
return
}
if preview.Count >= 3 {
if s.ImageOverflow {
t.Fatal("expected no overflow")
}
return
}
if !s.ImageOverflow || s.Warning == "" {
t.Fatalf("expected overflow warning, got overflow=%v warning=%q", s.ImageOverflow, s.Warning)
}
if !strings.Contains(s.Warning, "Bilder") {
t.Fatalf("unexpected warning: %s", s.Warning)
}
if len(s.Assignments) != preview.Count {
t.Fatalf("expected %d assignments, got %d", preview.Count, len(s.Assignments))
}
}
func TestCollectImageRefsOrder(t *testing.T) {
orders := []model.Order{
{ID: "o1", Images: []model.OrderImage{{ID: "a"}, {ID: "b"}}},
{ID: "o2", Images: []model.OrderImage{{ID: "c"}}},
}
refs := CollectImageRefs(orders)
if len(refs) != 3 || refs[0].ImageID != "a" || refs[2].OrderID != "o2" {
t.Fatalf("unexpected refs: %+v", refs)
}
}
+124
View File
@@ -0,0 +1,124 @@
package printjob
import (
"fmt"
"os"
"printer.backend/internal/itemimage"
"printer.backend/internal/layout"
"printer.backend/internal/model"
"printer.backend/internal/platepdf"
"printer.backend/internal/store"
)
// RenderPDF builds a plate PDF with customer images placed in each item mask.
func RenderPDF(
job model.PrintJob,
configs *store.ConfigurationStore,
plates *store.PlateStore,
items *store.ItemStore,
orders *store.OrderStore,
) ([]byte, model.PrintJobSummary, error) {
cfg, err := configs.Get(job.ConfigurationID)
if err != nil {
return nil, model.PrintJobSummary{}, fmt.Errorf("configuration: %w", err)
}
plate, err := findPlate(plates, cfg.PlateID)
if err != nil {
return nil, model.PrintJobSummary{}, err
}
item, err := items.Get(cfg.ItemID)
if err != nil {
return nil, model.PrintJobSummary{}, fmt.Errorf("item: %w", err)
}
itemSVGPath, err := items.SVGPath(cfg.ItemID)
if err != nil {
return nil, model.PrintJobSummary{}, err
}
itemSVG, err := os.ReadFile(itemSVGPath)
if err != nil {
return nil, model.PrintJobSummary{}, fmt.Errorf("read item svg: %w", err)
}
orderList, err := loadOrders(orders, job.OrderIDs)
if err != nil {
return nil, model.PrintJobSummary{}, err
}
preview := layout.Pack(plate, item.Spec, cfg.SpacingMM)
preview.PlateID = cfg.PlateID
preview.ItemID = cfg.ItemID
summary := Summary(orderList, preview)
slotSVGs, err := buildSlotSVGs(itemSVG, item.Spec, preview, summary.Assignments, orders)
if err != nil {
return nil, summary, err
}
pdf, err := platepdf.GenerateWithSlots(plate, item.Spec, slotSVGs, preview)
if err != nil {
return nil, summary, err
}
return pdf, summary, nil
}
func buildSlotSVGs(
templateSVG []byte,
spec model.ItemSpec,
preview model.LayoutPreview,
assignments []model.PrintJobSlotAssignment,
orders *store.OrderStore,
) ([][]byte, error) {
slotSVGs := make([][]byte, len(preview.Positions))
for i := range slotSVGs {
slotSVGs[i] = templateSVG
}
for _, a := range assignments {
if a.Slot < 0 || a.Slot >= len(slotSVGs) {
return nil, fmt.Errorf("slot %d out of range", a.Slot)
}
path, img, err := orders.ImagePath(a.Image.OrderID, a.Image.ImageID)
if err != nil {
return nil, fmt.Errorf("slot %d: %w", a.Slot, err)
}
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("slot %d: read image: %w", a.Slot, err)
}
embedded, err := itemimage.Embed(templateSVG, spec, data, img.ContentType)
if err != nil {
return nil, fmt.Errorf("slot %d: %w", a.Slot, err)
}
slotSVGs[a.Slot] = embedded
}
return slotSVGs, nil
}
func loadOrders(orders *store.OrderStore, ids []string) ([]model.Order, error) {
if len(ids) == 0 {
return nil, fmt.Errorf("at least one order_id is required")
}
out := make([]model.Order, 0, len(ids))
for _, id := range ids {
o, err := orders.Get(id)
if err != nil {
return nil, fmt.Errorf("order %s: %w", id, err)
}
out = append(out, o)
}
return out, nil
}
func findPlate(plates *store.PlateStore, id string) (model.Plate, error) {
list, err := plates.List()
if err != nil {
return model.Plate{}, err
}
for _, p := range list {
if p.ID == id {
return p, nil
}
}
return model.Plate{}, fmt.Errorf("plate not found: %s", id)
}