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"` PaperSize string `json:"paper_size"` } var reSVG = regexp.MustCompile(`([MLQCZmlqcz])|(-?\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}) pdf.AddPage() drawProblem(pdf, req, 0, rect.W, rect.H/2) drawProblem(pdf, req, rect.H/2, rect.W, rect.H/2) var buf bytes.Buffer _, err := pdf.WriteTo(&buf) return buf.Bytes(), err } func drawProblem(pdf *gopdf.GoPdf, req CountingRequest, startY, pW, pH float64) { margin := 40.0 boxW := (pW - 2*margin) * 0.7 boxH := pH - 60.0 x, y := margin, startY + 30.0 // 1. 绘制方框 pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(2.0) pdf.RectFromUpperLeft(x, y, boxW, boxH) // 2. 分配图标数量 numTypes := req.IconTypes; if numTypes < 1 { numTypes = 1 } if numTypes > len(data.CountingIcons) { numTypes = len(data.CountingIcons) } total := req.TotalCount; if total < numTypes { total = numTypes } if total > 30 { total = 30 } // 随机选出图标种类 allIcons := rand.Perm(len(data.CountingIcons)) selectedIcons := []data.Icon{} counts := []int{} // 先每种分配1个 rem := total - numTypes for i := 0; i < numTypes; i++ { selectedIcons = append(selectedIcons, data.CountingIcons[allIcons[i]]) counts = append(counts, 1) } // 随机分配剩下的 for i := 0; i < rem; i++ { counts[rand.Intn(numTypes)]++ } // 3. 布局计划 (使用网格,增加 Padding 避开边框) rows, cols := 5, 6 cellW, cellH := boxW/float64(cols), boxH/float64(rows) indices := rand.Perm(rows * cols) // 严格图标大小:单元格的 70%,确保不溢出单元格边界 iconSize := math.Min(cellW, cellH) * 0.7 paddingX, paddingY := (cellW - iconSize)/2, (cellH - iconSize)/2 iconTypeIdx := 0 countInCurrentType := 0 for i := 0; i < total; i++ { cellIdx := indices[i] rIdx, cIdx := cellIdx/cols, cellIdx%cols // 基础位置 baseX := x + float64(cIdx)*cellW + paddingX baseY := y + float64(rIdx)*cellH + paddingY // 渲染图标 drawIcon(pdf, selectedIcons[iconTypeIdx], baseX + iconSize/2, baseY + iconSize/2, iconSize) countInCurrentType++ if countInCurrentType >= counts[iconTypeIdx] { iconTypeIdx++ countInCurrentType = 0 } } // 4. 绘制右侧对照表 legendX := x + boxW + 20.0 legendStepY := boxH / float64(numTypes + 1) for i := 0; i < numTypes; i++ { lY := y + float64(i+1)*legendStepY drawIcon(pdf, selectedIcons[i], legendX + 20, lY, 30) pdf.SetStrokeColor(150, 150, 150); pdf.SetLineWidth(0.8) pdf.RectFromUpperLeft(legendX + 50, lY - 15, 30, 30) } } func drawIcon(pdf *gopdf.GoPdf, icon data.Icon, cX, cY, size float64) { scale := size / 1024.0 pdf.SetStrokeColor(0, 0, 0); pdf.SetLineWidth(1.5) // 加粗线条 ox, oy := cX - size/2, cY - size/2 for _, pStr := range icon.Paths { pts := parseSmoothPath(pStr, scale, ox, oy) if len(pts) > 1 { // 如果是闭合路径 if strings.Contains(strings.ToUpper(pStr), "Z") { pdf.Polygon(pts, "D") } else { for i := 0; i < len(pts)-1; i++ { pdf.Line(pts[i].X, pts[i].Y, pts[i+1].X, pts[i+1].Y) } } } } } // 支持贝塞尔曲线采样,确保图标圆润 func parseSmoothPath(path string, scale, ox, oy float64) []gopdf.Point { var pts []gopdf.Point matches := reSVG.FindAllStringSubmatch(path, -1) var lx, ly float64 for i := 0; i < len(matches); { cmd := matches[i][0] if cmd == "M" || cmd == "L" { x, _ := strconv.ParseFloat(matches[i+1][0], 64); y, _ := strconv.ParseFloat(matches[i+2][0], 64) lx, ly = x, y pts = append(pts, gopdf.Point{X: ox + x*scale, Y: oy + y*scale}) i += 3 } else if cmd == "C" { x1, _ := strconv.ParseFloat(matches[i+1][0], 64); y1, _ := strconv.ParseFloat(matches[i+2][0], 64) x2, _ := strconv.ParseFloat(matches[i+3][0], 64); y2, _ := strconv.ParseFloat(matches[i+4][0], 64) x, _ := strconv.ParseFloat(matches[i+5][0], 64); y, _ := strconv.ParseFloat(matches[i+6][0], 64) for t := 0.25; t <= 1.0; t += 0.25 { 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 += 7 } else if cmd == "Q" { x1, _ := strconv.ParseFloat(matches[i+1][0], 64); y1, _ := strconv.ParseFloat(matches[i+2][0], 64) x, _ := strconv.ParseFloat(matches[i+3][0], 64); y, _ := strconv.ParseFloat(matches[i+4][0], 64) 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 += 5 } else { i++ } } return pts }