Added Printjobs
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user