package logic import ( "bytes" "math" "math/rand" "regexp" "strconv" "strings" "time" "toolbox/pkg/learnnumber/data" "github.com/signintech/gopdf" ) type CountingRequest struct { TotalCount int `json:"total_count"` IconTypes int `json:"icon_types"` PageCount int `json:"page_count"` Category string `json:"category"` PaperSize string `json:"paper_size"` } type PlacedIcon struct { X, Y, Radius float64 } type PathResult struct { Points []gopdf.Point Closed bool } var reSVGToken = regexp.MustCompile(`[a-zA-Z]|-?\d+\.?\d*`) func GenerateCountingPDF(req CountingRequest) ([]byte, error) { rand.Seed(time.Now().UnixNano()) pdf := &gopdf.GoPdf{} rect := gopdf.Rect{W: 595.28, H: 841.89} if req.PaperSize == "Letter" { rect = gopdf.Rect{W: 612, H: 792} } pdf.Start(gopdf.Config{PageSize: rect}) // 硬性限制:页数 1-10 页,防止资源耗尽 if req.PageCount < 1 { req.PageCount = 1 } if req.PageCount > 10 { req.PageCount = 10 } for i := 0; i < req.PageCount; i++ { pdf.AddPage() drawProblemV4(pdf, req, 0, rect.W, rect.H/2) drawProblemV4(pdf, req, rect.H/2, rect.W, rect.H/2) } var buf bytes.Buffer _, err := pdf.WriteTo(&buf) return buf.Bytes(), err } func drawProblemV4(pdf *gopdf.GoPdf, req CountingRequest, startY, pW, pH float64) { margin := 40.0 boxW, boxH := (pW-2*margin)*0.7, pH-60.0 xBase, yBase := margin, startY + 30.0 pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(2.5) pdf.RectFromUpperLeft(xBase, yBase, boxW, boxH) catIcons := data.IconCategories[req.Category] if len(catIcons) == 0 { catIcons = data.IconCategories["shapes"] } numTypes := req.IconTypes if numTypes < 1 { numTypes = 1 }; if numTypes > 6 { numTypes = 6 } if numTypes > len(catIcons) { numTypes = len(catIcons) } total := req.TotalCount if total < numTypes { total = numTypes }; if total > 30 { total = 30 } allIconsPerm := rand.Perm(len(catIcons)) var selectedIcons []data.Icon counts := make([]int, numTypes) // 随机分配逻辑 for i := 0; i < numTypes; i++ { selectedIcons = append(selectedIcons, catIcons[allIconsPerm[i]]) counts[i] = 1 } remaining := total - numTypes for i := 0; i < remaining; i++ { counts[rand.Intn(numTypes)]++ } avgRadius := math.Sqrt((boxW * boxH * 0.22) / (float64(total) * math.Pi)) if avgRadius > 35.0 { avgRadius = 35.0 } var placed []PlacedIcon iconTypeIdx, currentInType := 0, 0 for i := 0; i < total; i++ { scaleVar := 0.9 + rand.Float64()*0.2; r := avgRadius * scaleVar for retry := 0; retry < 200; retry++ { randX := xBase + r + 5 + rand.Float64()*(boxW-2*r-10) randY := yBase + r + 5 + rand.Float64()*(boxH-2*r-10) collision := false for _, p := range placed { if math.Sqrt(math.Pow(randX-p.X, 2)+math.Pow(randY-p.Y, 2)) < (r + p.Radius + 10.0) { collision = true; break } } if !collision { drawSimpleIcon(pdf, selectedIcons[iconTypeIdx], randX, randY, r*2) placed = append(placed, PlacedIcon{X: randX, Y: randY, Radius: r}); break } } currentInType++; if currentInType >= counts[iconTypeIdx] { iconTypeIdx++; currentInType = 0 } } legendX, legendStepY := xBase+boxW+20.0, boxH/float64(numTypes+1) for i := 0; i < numTypes; i++ { lY := yBase + float64(i+1)*legendStepY drawSimpleIcon(pdf, selectedIcons[i], legendX+25, lY, 35) pdf.SetStrokeColor(150, 150, 150); pdf.SetLineWidth(1.0); pdf.RectFromUpperLeft(legendX+60, lY-15, 35, 35) } } func drawSimpleIcon(pdf *gopdf.GoPdf, icon data.Icon, cX, cY, size float64) { rawResults := parseMultiPath(icon, 1.0, 0, 0) if len(rawResults) == 0 { return } var minX, minY, maxX, maxY float64 = 1e9, 1e9, -1e9, -1e9 for _, res := range rawResults { for _, p := range res.Points { if p.X < minX { minX = p.X }; if p.X > maxX { maxX = p.X } if p.Y < minY { minY = p.Y }; if p.Y > maxY { maxY = p.Y } } } curW, curH := maxX-minX, maxY-minY if curW <= 0 { curW = 1 }; if curH <= 0 { curH = 1 } scale := size / math.Max(curW, curH) ox, oy := cX-(curW*scale)/2-minX*scale, cY-(curH*scale)/2-minY*scale pdf.SetStrokeColor(0, 0, 0) if strings.Contains(strings.ToLower(icon.Name), "item") || len(icon.Paths) > 1 { pdf.SetLineWidth(size / 60.0) // 复杂素材用细线 (30% less than 45 is approx 60) } else { pdf.SetLineWidth(size / 22.0) // 基础图形用粗线 } for _, res := range rawResults { scaledPts := make([]gopdf.Point, len(res.Points)) for i, p := range res.Points { scaledPts[i] = gopdf.Point{X: ox + p.X*scale, Y: oy + p.Y*scale} } if len(scaledPts) > 1 { if res.Closed { pdf.Polygon(scaledPts, "D") } else { for j := 0; j < len(scaledPts)-1; j++ { pdf.Line(scaledPts[j].X, scaledPts[j].Y, scaledPts[j+1].X, scaledPts[j+1].Y) } } } } } func parseMultiPath(icon data.Icon, scale, ox, oy float64) []PathResult { var results []PathResult for _, path := range icon.Paths { tokens := reSVGToken.FindAllString(path, -1) var lx, ly, startX, startY float64 var pts []gopdf.Point var currentCmd string var isRel bool for i := 0; i < len(tokens); { token := tokens[i] if (token[0] >= 'a' && token[0] <= 'z') || (token[0] >= 'A' && token[0] <= 'Z') { cmd := strings.ToUpper(token) if cmd == "M" { if len(pts) > 0 { results = append(results, PathResult{Points: pts, Closed: false}); pts = nil } } else if cmd == "Z" { if len(pts) > 0 { results = append(results, PathResult{Points: pts, Closed: true}); pts = nil } lx, ly = startX, startY; i++; continue } currentCmd = cmd; isRel = (token[0] >= 'a' && token[0] <= 'z'); i++ continue } switch currentCmd { case "M", "L": if i+1 >= len(tokens) { i = len(tokens); break } x, _ := strconv.ParseFloat(tokens[i], 64); y, _ := strconv.ParseFloat(tokens[i+1], 64) if isRel { x += lx; y += ly } if currentCmd == "M" { startX, startY = x, y } lx, ly = x, y pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + y*scale}); i += 2 if currentCmd == "M" { currentCmd = "L" } case "H": x, _ := strconv.ParseFloat(tokens[i], 64); if isRel { x += lx }; lx = x pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + ly*scale}); i++ case "V": y, _ := strconv.ParseFloat(tokens[i], 64); if isRel { y += ly }; ly = y pts = append(pts, gopdf.Point{X: ox + lx*scale, Y: oy + y*scale}); i++ case "C": if i+5 >= len(tokens) { i = len(tokens); break } x1, _ := strconv.ParseFloat(tokens[i], 64); y1, _ := strconv.ParseFloat(tokens[i+1], 64) x2, _ := strconv.ParseFloat(tokens[i+2], 64); y2, _ := strconv.ParseFloat(tokens[i+3], 64) x, _ := strconv.ParseFloat(tokens[i+4], 64); y, _ := strconv.ParseFloat(tokens[i+5], 64) if isRel { x1 += lx; y1 += ly; x2 += lx; y2 += ly; x += lx; y += ly } for t := 0.2; t <= 1.0; t += 0.2 { tx := math.Pow(1-t, 3)*lx + 3*math.Pow(1-t, 2)*t*x1 + 3*(1-t)*math.Pow(t, 2)*x2 + math.Pow(t, 3)*x ty := math.Pow(1-t, 3)*ly + 3*math.Pow(1-t, 2)*t*y1 + 3*(1-t)*math.Pow(t, 2)*y2 + math.Pow(t, 3)*y pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale}) } lx, ly = x, y; i += 6 case "Q": if i+3 >= len(tokens) { i = len(tokens); break } x1, _ := strconv.ParseFloat(tokens[i], 64); y1, _ := strconv.ParseFloat(tokens[i+1], 64) x, _ := strconv.ParseFloat(tokens[i+2], 64); y, _ := strconv.ParseFloat(tokens[i+3], 64) if isRel { x1 += lx; y1 += ly; x += lx; y += ly } for t := 0.25; t <= 1.0; t += 0.25 { tx := math.Pow(1-t, 2)*lx + 2*(1-t)*t*x1 + math.Pow(t, 2)*x ty := math.Pow(1-t, 2)*ly + 2*(1-t)*t*y1 + math.Pow(t, 2)*y pts = append(pts, gopdf.Point{X: ox + tx*scale, Y: oy + ty*scale}) } lx, ly = x, y; i += 4 case "A": i += 7 default: i++ } } if len(pts) > 0 { results = append(results, PathResult{Points: pts, Closed: false}) } } return results }