Files

105 lines
3.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, `<svg xmlns="http://www.w3.org/2000/svg" width="%.2fmm" height="%.2fmm" viewBox="0 0 %.2f %.2f"><rect width="100%%" height="100%%" fill="white"/><g font-family="sans-serif" fill="#222">`, width, height, width, height)
for i, line := range titleLines {
fmt.Fprintf(w, `<text x="10" y="%.2f" font-size="4">%s</text>`, 8+float64(i)*5, html.EscapeString(line))
}
fmt.Fprintf(w, `<text x="10" y="%.2f" font-size="2">%d × %d / %d beads</text>`, 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, `<rect x="%.2f" y="%.2f" width="%.2f" height="%.2f" fill="%s" stroke="#666" stroke-width="0.15"/><text x="%.2f" y="%.2f" font-size="%.2f" text-anchor="middle" fill="black" stroke="white" stroke-width="0.06" paint-order="stroke">%s</text>`, 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, `<text x="%.2f" y="%.2f" font-size="1.7">%d</text>`, 15+float64(x)*pitch, gridTop-1, x+1)
}
for y := 0; y < p.Height; y++ {
fmt.Fprintf(w, `<text x="8" y="%.2f" font-size="1.7">%d</text>`, gridTop+float64(y)*pitch+pitch/2, y+1)
}
baseline := gridTop + float64(p.Height)*pitch + 8
fmt.Fprintf(w, `<path d="M15 %.2f h50 m-50 -2 v4 m50 -4 v4" stroke="black" fill="none"/><text x="15" y="%.2f" font-size="3">50 mm calibration</text>`, baseline, baseline+7)
keys := []string{}
for code := range counts {
keys = append(keys, code)
}
sort.Strings(keys)
fmt.Fprintf(w, `<text x="15" y="%.2f" font-size="3">Print at 100%%. Disable fit-to-page.</text>`, baseline+13)
for i, code := range keys {
fmt.Fprintf(w, `<text x="15" y="%.2f" font-size="3">%s: %d</text>`, baseline+20+float64(i)*5, html.EscapeString(code), counts[code])
}
if width > 210 || height > 297 {
fmt.Fprintf(w, `<text x="15" y="%.2f" font-size="3">Oversize: use poster/tile mode.</text><text x="15" y="%.2f" font-size="3">Keep 100%% scale and add overlap.</text>`, height-13, height-8)
}
_, e := fmt.Fprint(w, "</g></svg>")
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)
}