105 lines
3.7 KiB
Go
105 lines
3.7 KiB
Go
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)
|
||
}
|