package server import ( "fmt" "github.com/gin-gonic/gin" "html" "io" "math" "os" "perler-beads-server/internal/domain" "sort" "strconv" "time" ) func wrapText(text string, limit int) []string { if limit < 1 { limit = 1 } r := []rune(text) out := []string{} for len(r) > limit { out = append(out, string(r[:limit])) r = r[limit:] } return append(out, string(r)) } func renderSVG(w io.Writer, p domain.Pattern, pitch float64) error { width := math.Max(100, float64(p.Width)*pitch+30) titleLines := wrapText(p.Title, int((width-20)/4)) gridTop := 20 + float64(len(titleLines)-1)*5 height := gridTop + float64(p.Height)*pitch + 70 + float64(len(p.Colors))*5 fmt.Fprintf(w, ``, width, height, width, height) for i, line := range titleLines { fmt.Fprintf(w, `%s`, 8+float64(i)*5, html.EscapeString(line)) } fmt.Fprintf(w, `%d × %d / %d beads`, gridTop-7, p.Width, p.Height, p.BeadCount) colors := map[string]string{} counts := map[string]int{} for _, c := range p.Colors { colors[c.Code] = c.Hex } for i, v := range p.Cells { x := float64(i%p.Width)*pitch + 15 y := float64(i/p.Width)*pitch + gridTop fill := "#fff" label := "" if v != nil { fill = colors[*v] label = *v counts[*v]++ } fmt.Fprintf(w, `%s`, x, y, pitch, pitch, fill, x+pitch/2, y+pitch*.6, math.Min(math.Min(pitch*.2, 1.5), pitch*.85/math.Max(1, float64(len([]rune(label)))*.65)), html.EscapeString(label)) } for x := 0; x < p.Width; x++ { fmt.Fprintf(w, `%d`, 15+float64(x)*pitch, gridTop-1, x+1) } for y := 0; y < p.Height; y++ { fmt.Fprintf(w, `%d`, gridTop+float64(y)*pitch+pitch/2, y+1) } baseline := gridTop + float64(p.Height)*pitch + 8 fmt.Fprintf(w, `50 mm calibration`, baseline, baseline+7) keys := []string{} for code := range counts { keys = append(keys, code) } sort.Strings(keys) fmt.Fprintf(w, `Print at 100%%. Disable fit-to-page.`, baseline+13) for i, code := range keys { fmt.Fprintf(w, `%s: %d`, baseline+20+float64(i)*5, html.EscapeString(code), counts[code]) } if width > 210 || height > 297 { fmt.Fprintf(w, `Oversize: use poster/tile mode.Keep 100%% scale and add overlap.`, height-13, height-8) } _, e := fmt.Fprint(w, "") return e } func (s *Server) print(c *gin.Context) { p, yes := s.readPattern(c) if !yes { return } pitch, e := strconv.ParseFloat(c.DefaultQuery("pitchMm", "5"), 64) if e != nil || math.IsNaN(pitch) || math.IsInf(pitch, 0) || pitch < 2 || pitch > 10 { reply(c, 400, "pitchMm 需为 2–10", nil) return } f, e := os.CreateTemp("", "perler-print-*.svg") if e != nil { fail(c, e) return } path := f.Name() defer func() { time.AfterFunc(time.Minute, func() { os.Remove(path) }) }() if e = renderSVG(f, p, pitch); e != nil { f.Close() fail(c, e) return } f.Close() c.Header("Content-Disposition", `attachment; filename="perler-pattern.svg"`) c.Header("Content-Type", "image/svg+xml") c.File(path) }